# How to dowload youtube video? ###### tags: `Youtube` `trace code` ## Preface Youtube 似乎有改過一些機制,參考時間早一些的 code 都是用 `https://www.youtube.com/get_video_info?videoid={id}` 去取得相關資訊,然後直接下載。現在似乎不能用了,雖然沒有找到官方的文件。 youtube-dl 因為有聽過,想從這個開始看,不過看了後覺得有點複雜,先跳過。 ## Flow Take [youtube](https://github.com/kkdai/youtube) as an example. 1. Get video info via innertube 2. Execute javascript function 3. Dowload chunk by chunk 4. Consider use FFMPEG to merge ### Get video info via innertube 關於 Innertube 我沒有找到官方的文件。不過網路上還是有些描述<sup>[1](https://github.com/kkdai/youtube/issues/226) [2](https://gizmodo.com/how-project-innertube-helped-pull-youtube-out-of-the-gu-1704946491)</sup>。 總之就是個某個可以去得影片資訊的管道就是了。 ```go var ( webClient = clientInfo{ name: "WEB", version: "2.20210617.01.00", key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", } ) func (c *Client) videoDataByInnertube(ctx context.Context, id string, clientInfo clientInfo) ([]byte, error) { data := innertubeRequest{ VideoID: id, } var url = "https://www.youtube.com/youtubei/v1/player?key="+clientInfo.key return c.httpPostBodyBytes(ctx, url, data) } ``` 剛看到還在想作者怎麼會把自己的 API key 寫在 code 裡面,後來發現這是 innertube 的 key,很多實作都有用到,不知道是怎麼來的。 ### Execute javascript function ```go var basejsPattern = regexp.MustCompile(`(/s/player/\w+/player_ias.vflset/\w+/base.js)`) func (c *Client) getPlayerConfig(ctx context.Context, videoID string) (playerConfig, error) { embedURL := fmt.Sprintf("https://youtube.com/embed/%s?hl=en", videoID) embedBody, err := c.httpGetBodyBytes(ctx, embedURL) // example: /s/player/f676c671/player_ias.vflset/en_US/base.js playerPath := string(basejsPattern.Find(embedBody)) config, err = c.httpGetBodyBytes(ctx, "https://youtube.com"+playerPath) return config, nil } func evalJavascript(jsFunction, arg string) (string, error) { vm := goja.New() _, err := vm.RunString(myName + "=" + jsFunction) var output func(string) string err = vm.ExportTo(vm.Get(myName), &output) return output(arg), nil } ``` 要去 base.js 裡解析出一些 javascript 的程式碼,並對某些資料進行轉換。這個實作是用 [goja](https://github.com/dop251/goja) 來完成的。 ### Dowload chunk by chunk ```go func (c *Client) downloadChunked(req *http.Request, w *io.PipeWriter, format *Format) { loadChunk := func(pos int64) (int64, error) { req.Header.Set("Range", fmt.Sprintf("bytes=%v-%v", pos, pos+chunkSize-1)) resp, err := c.httpDo(req) defer resp.Body.Close() return io.Copy(w, resp.Body) } defer w.Close() for pos := int64(0); pos < format.ContentLength; { written, err := loadChunk(pos) if err != nil { w.CloseWithError(err) return } pos += written } } ``` 對剛才解析出來的 url 一塊一塊下載([byte range](https://www.rfc-editor.org/rfc/rfc2616#page-138))。 這裡是用 HTTP GET 來抓,不過從 dev tool 來看 youtube 是用 HTTP POST 來抓影片。 ```go func (c *Client) GetStreamContext(ctx context.Context, video *Video, format *Format) (io.ReadCloser, int64, error) { url, err := c.GetStreamURL(video, format) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) r, w := io.Pipe() go c.downloadChunked(req, w, format) return r, contentLength, nil } func (dl *Downloader) videoDLWorker(ctx context.Context, out *os.File, video *youtube.Video, format *youtube.Format) error { stream, size, err := dl.GetStreamContext(ctx, video, format) progress := mpb.New(mpb.WithWidth()) bar := progress.AddBar() reader := bar.ProxyReader(stream) mw := io.MultiWriter(out, prog) _, err = io.Copy(mw, reader) } ``` 這裡是先建立一個 pipe,再建立一個 goroutine 去下載影片(write to pipe),外面再都用`io.MultiWriter` 把 pipe read 跟 progress bar 綁定。這其實跟主題沒什麼關係,只是沒那麼常用 golang ,稍微停下來看個兩眼。 ### Consider use FFMPEG to merge ```go func download(id string) error { video, format, err := getVideoWithFormat(id) if strings.HasPrefix(outputQuality, "hd") { return downloader.DownloadComposite(context.Background(), outputFile, video, outputQuality, mimetype) } return downloader.Download(context.Background(), video, format, outputFile) } ``` 需不需要用 FFMPEG 合成 audio 跟 video 跟畫質有關。 ## Murmur 蠻多細節其實我並不是那麼關心,因為 youtube 跟 client 之間的行為並不是什麼標準,也隨時都可能改。 ## Reference [youtube-dl](https://github.com/ytdl-org/youtube-dl) [YoutubeDownloader](https://github.com/Tyrrrz/YoutubeDownloader)