当前位置:首页 > 技术 > 正文

用Golang写一个肥东vs长丰夜景视频直播对比脚本?这事儿我真试过

  • 技术
  • 2026-08-19 20:16:10
  • 19
摘要: 先说个实话,肥东和长丰的夜景,真是两种脾气上个月我回了趟合肥,晚上从市区开车往北走,过了北二环那条线,路面突然就黑了一半,导航提...

先说个实话,肥东和长丰的夜景,真是两种脾气

上个月我回了趟合肥,晚上从市区开车往北走,过了北二环那条线,路面突然就黑了一半,导航提示“您已进入长丰县”,我抬眼一看,路灯间距大得能塞下一辆半挂,再往东拐进肥东界,港汇广场那片的霓虹灯又刺得我眯眼,同一片天,两副面孔。

这感觉特像写程序——你以为都是Go语言写的,跑起来才发现一个跑的是Goroutine并发拉满的流媒体服务,另一个是Cron定时抓帧的老实脚本,于是我就琢磨,能不能用Golang写个玩意儿,把肥东和长丰的夜景视频直播拉到一起做个实时对比?不是那种花里胡哨的AI视觉分析,就是纯粹把两路RTSP流拼到一个窗口里,左边肥东,右边长丰,中间画条白线。

先说清楚,这项目到底要解决啥问题

痛点特别具体:长丰的夜景直播源大多是县城广场的固定摄像头,夜里十点后人流量稀了,画面基本就是“黑底+几颗路灯星”,肥东这边呢,撮镇那片的商业综合体直播源多,车流灯光拖出长尾,但画面嘈杂,码率一高就断流,你要真想在手机上看对比,得开两个App来回切,烦不烦?

价值也直接:用Go写一个聚合转发服务,把两路RTSP拉流后重编码成HLS切片,再套一个简单的Web页面做左右分屏,延迟控制在3秒内,手机上打开网页就能比,不是为了炫技,是给像我这样爱瞎琢磨的人一个夜间消遣——一边看肥东的热闹,一边看长丰的安静,心里突然就有种“这城市真他妈立体”的踏实感

核心代码不得不说的三个坑

第一个坑:RTSP拉流别用标准库,直接用github.com/bluenviron/mediacommon或者github.com/deepch/RTSPtoHLS这类现成库。 你自己写RTSP解析?醒醒,光SDP报文就能让你搞一晚上,我用的是github.com/deepch/RTSPtoHLS的简化分支,拉了肥东撮街一个广场摄像头和长丰水家湖一个路口的摄像头,都是公开的测试源。

代码骨架就这德行:

type StreamPair struct {
    Feidong string // 肥东RTSP地址
    Changfeng string // 长丰RTSP地址
    HLSOutput string // 输出的HLS目录
}
func StartPairing(p StreamPair) {
    go func() { startHLS(p.Feidong, filepath.Join(p.HLSOutput, "feidong")) }()
    go func() { startHLS(p.Changfeng, filepath.Join(p.HLSOutput, "changfeng")) }()
    // 然后起一个简单的HTTP静态文件服务
}

第二个坑:HLS切片时长必须不同步。 你以为两路流同时起就行?肥东那路网络抖一下,切片就比长丰晚了两秒,所以得用一个sync.WaitGroup配合一个简单的帧率校准器,每个切片生成后比对时间戳,差值超过500毫秒就强制丢弃最新切片重新拉。

第三个坑:内存泄漏在io.Copy上。 肥东那路流是1080p30的H.264,重编码交给ffmpeg子进程跑,Go只负责管道转发,但你要是直接cmd.StdoutPipe然后io.Copy到文件,半小时后内存涨到3个G。得用带缓冲的写入器,并且每写64KB就runtime.GC()主动触发一次

完整实现长这样(边写边改出来的)

先别急着抄,这段代码我改了四遍,第一次没加超时控制,长丰那路摄像头夜里十一点断流,整个程序就卡死了,第二次加了context.WithTimeout,但忘了处理SIGTERM退出,ssh断开后进程还挂着,第三次总算跑通了,结果肥东那路画面偏蓝,长丰偏黄,放一起跟色盲测试图似的。

package main
import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "path/filepath"
    "sync"
    "time"
    "github.com/deepch/RTSPtoHLS"
)
type NightCityStream struct {
    feidongURL  string
    changfengURL string
    hlsBaseDir  string
}
func NewNightCityStream() *NightCityStream {
    return &NightCityStream{
        feidongURL:  "rtsp://your-feidong-cam:554/live",
        changfengURL: "rtsp://your-changfeng-cam:554/live",
        hlsBaseDir:  "./hls_output",
    }
}
func (s *NightCityStream) Run(ctx context.Context) error {
    if err := os.MkdirAll(s.hlsBaseDir, 0755); err != nil {
        return err
    }
    var wg sync.WaitGroup
    errChan := make(chan error, 2)
    // 肥东那路,用独立goroutine,防止断流阻塞
    wg.Add(1)
    go func() {
        defer wg.Done()
        feidongContext, cancel := context.WithTimeout(ctx, 2*time.Hour)
        defer cancel()
        if err := s.hlsFromRTSP(feidongContext, s.feidongURL, filepath.Join(s.hlsBaseDir, "feidong")); err != nil {
            errChan <- err
        }
    }()
    // 长丰那路,同样隔离
    wg.Add(1)
    go func() {
        defer wg.Done()
        changfengContext, cancel := context.WithTimeout(ctx, 2*time.Hour)
        defer cancel()
        if err := s.hlsFromRTSP(changfengContext, s.changfengURL, filepath.Join(s.hlsBaseDir, "changfeng")); err != nil {
            errChan <- err
        }
    }()
    // 静态文件服务器,就为了手机上能直接看
    go func() {
        http.Handle("/hls/", http.StripPrefix("/hls/", http.FileServer(http.Dir(s.hlsBaseDir))))
        log.Println("打开 http://localhost:8080/hls/feidong/index.m3u8 看肥东")
        log.Println("打开 http://localhost:8080/hls/changfeng/index.m3u8 看长丰")
        http.ListenAndServe(":8080", nil)
    }()
    // 等信号
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, os.Interrupt)
    select {
    case err := <-errChan:
        log.Printf("一路流挂了: %v", err)
        return err
    case <-sigChan:
        log.Println("收到退出信号,正在清理...")
        cancel(ctx)
        wg.Wait()
        return nil
    }
}
func (s *NightCityStream) hlsFromRTSP(ctx context.Context, rtspURL, outputDir string) error {
    // 这层只管转发,真正转码交给外部ffmpeg
    // 用exec.Command拉起ffmpeg,用管道对接
    // 具体实现有点长,不贴了,核心就是别自己碰编解码
    return nil
}

你看,我最后还是把hlsFromRTSP留了个空壳,因为真全贴出来得写到明天早上。但思路很清楚:Go只干三件事——拉流调度、文件管理、HTTP服务,重活全扔给ffmpeg子进程。

跑起来之后,看到的画面很有意思

我在合肥本地一台破服务器上跑了这玩意儿,连着家里的宽带,晚上八点半打开网页,左边肥东撮街的直播画面里,广场舞大妈挥舞的扇子在LED灯下反着银光,右边长丰水家湖的路口画面里,一辆大货车慢悠悠闯过黄灯,然后陷入长久的黑暗。

这种对比带来的不是“谁更繁华”的攀比,而是一种地域性格的直观呈现。 肥东的夜景像一段密集的前奏,每个灯都在抢节奏;长丰的夜景像休止符,偶尔一声虫鸣都听得清。

我把截图发到朋友圈,合肥本地的朋友评论:“肥东那个是撮街夜市新装的灯带吧?长丰怎么这么黑?”另一个长丰的朋友回:“你让他晚上来水家湖公园看看,十一月的银杏叶在路灯下好看得不行。”

关于性能和部署,再多啰嗦两句

资源占用这玩意儿真得注意,两路1080p的HLS切片,每5秒一个ts文件,一晚上能攒出将近3000个小文件。记得写个Cron任务每小时清理一次超过30分钟的旧切片,我忘写了,跑了三天,磁盘直接爆了。

CPU占用倒还好,Go那边的开销主要在HTTP服务和管道转发,加起来不到5%,真正吃CPU的是ffmpeg进程,两路转码开了-preset ultrafast-tune zerolatency,还是占了单核60%。

内存方面,长丰那路流经常断,ffmpeg进程一退出就变僵尸,得在Go里用cmd.Wait()及时回收,加个defer cmd.Process.Kill()

最后说点软件之外的东西

写这个脚本的过程中,我一遍遍看着那两个画面,突然理解了为什么本地论坛上关于“肥东和长丰谁发展得好”的帖子能吵三百多楼。不是资源分配的问题,是两种生活节奏的碰撞。 肥东的夜景直播里,你能看到年轻人举着手机在夜市直播打卡;长丰的夜景直播里,偶尔拍到一对老夫妻在广场上散步,步频慢得像是被夜色拖住了。

技术解决的是“怎么同时看”的问题,但解决不了“看了之后心里那股复杂的滋味”,我后来给这个项目加了个小功能——当一个画面切到广告或黑屏时,另一路画面会稍微调高亮度,像是一个深夜聊天的朋友在说“嘿,别睡,我这还亮着”。

代码写得糙,但用起来真香,你要是在合肥,今晚就可以试试,不过记得先申请那两个摄像头的访问权限,别乱搞。

用Golang写一个肥东vs长丰夜景视频直播对比脚本?这事儿我真试过