# 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)