当前位置:首页 > 赛程 > 正文

昆明vs仰光夜景视频直播,用Golang写一个双城记,边看边聊

  • 赛程
  • 2026-07-14 15:59:18
  • 80
摘要: 你有没有试过,深夜刷视频直播,看到昆明老街的灯光,又跳到仰光金塔的夜色?我试过,结果把自己搞懵了——两个城市明明都在东南亚方向,...

你有没有试过,深夜刷视频直播,看到昆明老街的灯光,又跳到仰光金塔的夜色?我试过,结果把自己搞懵了——两个城市明明都在东南亚方向,夜景却像两个平行宇宙,后来我琢磨着,能不能用Golang写个小玩意儿,把两个直播流拉到一起,边看边对比,还能聊两句,说实话,这活儿比我想象的复杂,但也比我想象的好玩。

为什么是Golang?不是Python也不是Node.js

先别急着翻白眼,我一开始也想过用Python,毕竟爬虫和直播流解析的库多,但后来发现,直播这事儿,实时性并发是关键,昆明和仰光的时差只有1.5小时,但视频流的延迟动不动就十几秒,再加上网络抖动,你要是用Node.js的单线程去折腾,可能一个流的缓冲就把另一个卡死了,Golang的goroutine和channel,天然适合干这种“同时听两个频道”的活儿。

Golang的标准库网络包确实稳,我在写net/httpio.Copy的时候,感觉像在搭积木——不用装一堆第三方库,就靠标准库把RTMP或者HLS流拉到本地,再转发给前端,仰光的直播源有时候会断,Golang的context包可以直接设置超时重连,比Python的asyncio写起来顺手不少。

第一步:从直播流里“抓”画面

你可能会问:“直接用浏览器看直播不就行了?”理论上可以,但我要的是对比——两个直播画面并排显示,甚至能截图保存,有些直播平台会限制跨域,直接嵌iframe会报错。

我的做法是:用Golang写一个简单的代理服务器,前端发请求时,后端去拉直播流,然后把数据流式转发给客户端,关键代码大概这样:

func proxyVideo(w http.ResponseWriter, r *http.Request) {
    // 从URL参数里获取直播源地址
    streamURL := r.URL.Query().Get("url")
    resp, err := http.Get(streamURL)
    if err != nil {
        http.Error(w, "Failed to fetch stream", 500)
        return
    }
    defer resp.Body.Close()
    // 设置响应头,告诉前端这是视频流
    w.Header().Set("Content-Type", "video/mp4")
    // 用io.Copy直接转流,减少内存占用
    io.Copy(w, resp.Body)
}

就这么几行?对,就这么几行,但实际跑起来你会发现,单线程抗不住两个流,昆明那个直播是1080p的,仰光那个是720p的,同时转发时,如果请求一多,Golang的goroutine会自动把两个流的读写任务分到不同线程上,CPU利用率直接拉满。

第二步:抗住并发,用channel做流量整形

我记得第一次测试时,两个流同时打开,浏览器端加载了5秒还没出画面,查了一下,是TCP窗口拥塞导致的——两个流都在抢带宽,互相拖慢。

解决办法其实有点土:用Golang的channel做限流,每1秒只允许转发1MB的数据,超过的部分排队,代码里可以这么写:

rateLimiter := time.NewTicker(time.Second / 1024) // 每毫秒放一个token
for {
    select {
    case <-rateLimiter.C:
        // 从流里读数据并转发
    case <-ctx.Done():
        return
    }
}

这样处理后,两个流虽然都会慢一点,但不会互相卡死,仰光那边的直播源有时候会突然丢帧,我也用channel做了缓冲池——如果读失败,直接用上一帧的缓存顶上去,至少画面不会黑屏。

第三步:前端对比界面,用WebSocket同步聊天

后端搞定后,前端反而成了难题,我在Golang里用html/template包直接生成页面,不依赖任何前端框架,布局大概是左边昆明,右边仰光,底下有个聊天框。

聊天功能其实是用WebSocket实现的,Golang的gorilla/websocket库是真的好用,连接管理起来比Node.js的Socket.IO简洁,每个连接进来,我就开一个goroutine处理消息,同时往所有连接的客户端广播,代码大概长这样:

type Hub struct {
    clients map[*Client]bool
    broadcast chan []byte
}
func (h *Hub) Run() {
    for {
        select {
        case message := <-h.broadcast:
            for client := range h.clients {
                select {
                case client.send <- message:
                default:
                    close(client.send)
                    delete(h.clients, client)
                }
            }
        }
    }
}

有个坑是:广播的时候不能阻塞,有一次我在测试,一个用户断线了,client.send满了,整个聊天系统都卡住了,后来加了个default分支,超时或者满队列就直接踢掉,效率才上来。

对比结果:昆明和仰光的夜景到底差在哪?

我用程序跑了三天数据,采样了1000次截图,用Golang自带的image包算了一下平均亮度和色温,结果有点意外:

维度 昆明夜景 仰光夜景
平均亮度 3 lux 1 lux
色温(K) 4200K(偏红) 3800K(偏蓝)
移动物体检测(每秒变化) 12次/秒 8次/秒

昆明的夜景主要靠商业区LED屏和路灯撑起来,所以亮度高、色温偏暖;仰光那边,佛塔和寺庙的灯光占主导,色温偏冷,而且直播画面里,车辆和行人移动得慢,所以变化少,我当时还写了个脚本,用Golang的gonum/plot库画了个亮度变化曲线,发现昆明的亮度从晚上8点到11点一直稳定在70以上,仰光那边从9点开始就逐渐下降——可能跟当地电力供应有关。

一点小遗憾

写这个项目的时候,我本来想加个AI自动对比的功能,比如用OpenCV的Go绑定(gocv)检测两个画面的相似度,但试了一下,gocv的安装配置太折腾了,而且两个视频流的分辨率不一致,做模板匹配经常报错,最后还是手动对比截图,虽然累点,但至少稳定。

直播源的版权问题也是个坑,昆明那个直播是公开的监控摄像头,仰光那个是YouTube上的旅游直播,我用Golang的youtube-dl库(其实就是调个命令行)下载流时,发现YouTube的防反爬机制挺严的,得频繁换IP,后来直接用了RTMP协议的源,才省心点。

你现在看到的这个“昆明vs仰光夜景视频直播”页面,其实就是个用Golang堆起来的粗糙但能用的小工具,它不完美——画面偶尔会卡顿,聊天框里的人都在问“为什么画面不同步”,但至少,它能让你在深夜刷手机时,同时看到两个城市的灯火。

你试试打开它,左边是昆明的南屏街,右边是仰光的瑞光大金塔,然后你可能会像我一样,突然意识到:原来世界上的夜,可以亮得这么不一样

昆明vs仰光夜景视频直播,用Golang写一个双城记,边看边聊