Youtube 是个大宝库!

前言

Youtube 直播上有很多好东西,比如各大电视台喜欢在油管上直播自己的新闻频道。

比如海量台湾电视新闻台都在油管上搞了直播,我整理了一下这些。

#三立LIVE新聞HD直播
https://www.youtube.com/watch?v=4ZVUmEUFwaY
#TVBS新聞 55 頻道
https://www.youtube.com/watch?v=Hu1FkdAOws0
#東森財經新聞 57
https://www.youtube.com/watch?v=dphWo0r27Z4
#東森新聞 51 頻道
https://www.youtube.com/watch?v=RaIJ767Bj_M
#CTI中天新聞HD直播
https://www.youtube.com/watch?v=wUPPkSANpyo
#中視新聞台 LIVE
https://www.youtube.com/watch?v=3OPNkiqD48g
#民視新聞直播
https://www.youtube.com/watch?v=XxJKnDLYZz4
#華視新聞HD
https://www.youtube.com/watch?v=TL8mmew3jb8

解析

我曾经想过用 Golang 自己写一套 Youtube 解析算法,但是考虑到我可能没有精力随时维护解析算法,所以还是要用现成的第三方库来弄。

Github 有个非常出名的第三方 Youtube 工具,youtube-dl。能在开源世界上拿到 65.6k 的 star 非常不容易, 也侧面证明了这个工具的靠谱程度。通过查看 youtube-dl 的帮助,可以知道 youtube-dl -f best -g {url} 能解析到 Youtube 的 M3U8。

在 Golang 中调用程序并获取输出内容,可以这么做:

_, err := exec.LookPath("youtube-dl")
if err != nil {
    return "", err
} else {
    ctx, cancelFunc := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancelFunc()
    cmd := exec.CommandContext(ctx, "youtube-dl", "-f","best","-g","${URL}")
    out, err := cmd.CombinedOutput()
    return strings.TrimSpace(string(out)), err
}

首先需要检查要调用的程序有没有,免得出问题。exec.LookPath 会检查能否找到程序,在 err == nil 时,证明可以调用到程序。 使用 exec.Command 指定调用的命令行,CombineOutput 会运行并输出程序运行内容。需要考虑的是,运行程序可能会卡住,尤其是 国内的神奇网络,如果没有扶墙,可能会无限卡下去。所以,必须增加超时。通过指定超时 context.WithTimeout(context.Background(), 10*time.Second), 设定 10 秒的延迟。所以这个时候需要把 exec.Command 换成 exec.CommandContext,超时之后会自动取消。

提供服务

Kodi 之类的 IPTV 客户端,都只能使用 M3U8 地址。所以需要一个 Go 程序提供 Youtube 直播地址转换到 M3U8 功能。用 Gin 可以非常容易的写一个 Web 服务。

func liveHandler(c *gin.Context) {
	liveURL := c.Query("url")
	if liveURL == "" {
		c.AbortWithStatus(http.StatusNotFound)
		return
	}
	liveM3U8, err := getYoutubeLiveM3U8(liveURL)
	if err != nil {
		log.Println(err)
		c.AbortWithStatus(http.StatusInternalServerError)
		return
	}
	if cfg.ProxyStream {
		liveProxyM3U8 := cfg.BaseURL + "/p/live.m3u8?url=" + url.QueryEscape(liveM3U8)
		c.Redirect(http.StatusTemporaryRedirect, liveProxyM3U8)
	} else {
		c.Redirect(http.StatusTemporaryRedirect, liveM3U8)
	}
}

代理直播流

正常的油管 M3U8 在电视之类没扶墙的环境是不能播放的,所以代理是很有必要的。如何代理可以参看 RTHK 那篇博文。

这种代理就是简单的读取,转发。只是 M3U8 里面的连接也跟着替换一下,注意必须保持扩展名 .m3u8 .ts , 要不然 VLC 之类的会无法播放。

优化加缓存

实际使用的时候,会发现一个很大的问题。比如 Kodi 换台的时候,首先会访问设定的地址,然后调用一次 youtube-dl, 解析个6-7秒返回结果,再加上 Kodi 需要去真实的直播源加载数据,每次换台需要卡顿 8-10s 左右。

考虑到频道列表是固定的,所以可以提前使用 youtube-dl 加载一下。要缓存数据,最好的方法是使用 redis 之类的工具。 不过为了这个简单的缓存引入 redis 实在不值当,所以直接使用内置类型 map 会很方便,高并发环境下需要使用 sync.Map 防止冲突 panic。

具体实现可以参考 https://github.com/zjyl1994/livetv/blob/master/m3u8cache.go 文件。

缓存规则

在进行 Youtube 地址转换获取 M3U8 的时候,先行访问快取,如果命中快取就直接用快取中获取。获取不到再去用真实的 youtube-dl 获取连接。

冷启动

当程序启动的时候,快取中没有任何数据,所以程序启动时需要逐个获取连接放入快取。

func loadChannelCache() {
	channels, err := channelParser(cfg.ChannelFile)
	if err != nil {
		log.Println(err)
		return
	}
	for _, v := range channels {
		log.Println("caching", v.URL)
		liveURL, err := realGetYoutubeLiveM3U8(v.URL)
		if err != nil {
			log.Println(err)
			return
		}
		urlCache.Store(v.URL, liveURL)
		log.Println(v.URL, "cached")
	}
}

热更新

Youtube 生成的 M3U8 中有过期时间,一般是6小时。如果想要保持随时可看的状态,就得经常更新。 编写一个更新函数,从频道定义文件里获取 Youtube 链接,然后更新一下所有频道的 M3U8。 然后使用 sync.Map.Range 遍历所有的链接,清理掉过期的链接,只要2-3小时抓取一次即可。

func updateURLCache() {
	channels, err := channelParser(cfg.ChannelFile)
	if err != nil {
		log.Println(err)
		return
	}
	for _, v := range channels {
		log.Println("caching", v.URL)
		liveURL, err := realGetYoutubeLiveM3U8(v.URL)
		if err != nil {
			log.Println(err)
		} else {
			urlCache.Store(v.URL, liveURL)
			log.Println(v.URL, "cached")
		}
	}
	urlCache.Range(func(k, v interface{}) bool {
		value := v.(string)
		regex := regexp.MustCompile(`/expire/(\d+)/`)
		matched := regex.FindStringSubmatch(value)
		if len(matched) < 2 {
			urlCache.Delete(k)
		}
		expireTime := time.Unix(string2Int64(matched[1]), 0)
		if time.Now().After(expireTime) {
			urlCache.Delete(k)
		}
		return true
	})
}

安装和使用

本次的程序使用多配置文件模式,具体可以参考 https://github.com/zjyl1994/livetv/blob/master/README-zh.md

这次折腾完了,就可以用 Kodi 看香港台,台湾台,等等各种直播频道,配置好之后就享受观影吧。