From 43a951a66878b4b4522f164f685e7fcd4f2b8ad7 Mon Sep 17 00:00:00 2001 From: qydysky Date: Sun, 13 Jun 2021 17:20:05 +0800 Subject: [PATCH] =?utf8?q?=E7=9B=B4=E6=92=AD=E6=B5=81=E6=9C=8D=E5=8A=A1?= =?utf8?q?=E6=94=AF=E6=8C=81hls=E7=9A=84mp4=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- README.md | 39 ++++++--- Reply/F.go | 238 +++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 204 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 8ae8706..16b294a 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ ass编码GB18030支持中文 - `utf-8` #### 直播流Web服务 -启动Web流服务,为下载的直播流提供局域网内的流服务。 +启动Web流服务,为下载的直播流提供局域网内的流服务,提供flv、hls/mp4格式流。 在`demo/config/config_K_v.json`中可找到配置项,0:随机可用端口 >0:固定可用端口 <0:禁用服务。 @@ -133,6 +133,12 @@ ass编码GB18030支持中文 开启之后,启动会显示服务地址,在局域网内打开网址可以取得所有直播流的串流地址 +服务地址也可通过命令行` room`查看。 + +``` +I: 2021/04/13 20:07:45 命令行操作 [直播Web服务: http://192.168.31.245:38259] +``` + 支持跨域,注意:在https网站默认无法加载非本机http服务 - dtmp结尾:当前正在获取的流,播放此链接时进度将保持当前流进度 @@ -140,22 +146,30 @@ ass编码GB18030支持中文 - ass结尾:保存完毕的直播流字幕,有些播放器会在串流时获取此文件 - m4s结尾:hls切片 -**特殊的:路径为`/now`(例:当服务地址为下方的38259口时,此对应的路径为`http://192.168.31.245:38259/now`),会重定向到当前正在获取的流,播放此链接时进度将保持当前流进度** +**特殊** +- 路径为`/now` -服务地址也可通过命令行` room`查看。 + 例:当服务地址为下方的38259口时,此对应的路径为`http://192.168.31.245:38259/now`),会重定向到当前正在获取的流,播放此链接时进度将保持当前流进度。流格式为hls或flv -``` -I: 2021/04/13 20:07:45 命令行操作 [直播Web服务: http://192.168.31.245:38259] -``` +- 当在hls流时,(已/正在)下载的流链接后加上`?type=mp4`将会得到拼合好的mp4流。 + + 例:直播流:`http://192.168.31.245:38259/now?type=mp4`的流格式为mp4。hls录播目录:`http://192.168.31.36:23333/1016_2021_06_12_01-18-59-000/?type=mp4`的流格式为mp4) 测试可用项目(测试可连续播放10min+): -- [xqq/mpegts.js](https://github.com/xqq/mpegts.js) -- [bilibili/flv.js](https://github.com/bilibili/flv.js) -- [bytedance/xgplayer](https://github.com/bytedance/xgplayer) -- [videojs/video.js](https://github.com/videojs/video.js)([demo](https://videojs-http-streaming.netlify.app)) -- [video-dev/hls.js@v1.0.7+](https://hls-js-10780deb-25d8-41d3-b164-bc334c8dd47f.netlify.app/demo/) -- [mpv](https://mpv.io/) +- flv-html播放器 + - [xqq/mpegts.js](https://github.com/xqq/mpegts.js) + - [bilibili/flv.js](https://github.com/bilibili/flv.js) +- hls-html播放器 + - [bytedance/xgplayer](https://github.com/bytedance/xgplayer) + - [videojs/video.js](https://github.com/videojs/video.js)([demo](https://videojs-http-streaming.netlify.app)) + - [video-dev/hls.js@v1.0.7+](https://hls-js-10780deb-25d8-41d3-b164-bc334c8dd47f.netlify.app/demo/) +- mp4-html播放器 + - [bytedance/xgplayer](https://github.com/bytedance/xgplayer) + - [videojs/video.js](https://github.com/videojs/video.js)([demo](https://videojs-http-streaming.netlify.app)) +- 客户端播放器 + - [mpv](https://mpv.io/) + - [MXPlayer](https://sites.google.com/site/mxvpen/home) #### 命令行操作 @@ -167,7 +181,6 @@ I: 2021/04/01 11:36:46 命令行操作 [房间信息->输入' room'回车] I: 2021/04/01 11:36:46 命令行操作 [开始结束录制->输入' rec'回车] I: 2021/04/01 11:36:46 命令行操作 [查看直播中主播->输入' live'回车] I: 2021/04/01 11:36:46 命令行操作 [其他输出隔断不影响] - ``` 用例: - 直播间切换 diff --git a/Reply/F.go b/Reply/F.go index 1cca6af..5fa7aa6 100644 --- a/Reply/F.go +++ b/Reply/F.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "io" + "io/fs" "strconv" "strings" "sync" @@ -240,8 +241,8 @@ type Savestream struct { b []byte//发送给客户的m3u8字节 t time.Time } - flv_front []byte//flv头及首tag - flv_stream *msgq.Msgq//发送给客户的flv流关键帧间隔片 + front []byte//flv头及首tag or hls的初始化m4s + stream *msgq.Msgq//发送给客户的flv流关键帧间隔片 or hls的fmp4片 m4s_hls int//hls list 中的m4s数量 hlsbuffersize int//hls list缓冲m4s数量 @@ -253,6 +254,7 @@ type Savestream struct { } type hls_generate struct { + hls_first_fmp4_name string hls_file_header []byte//发送给客户的m3u8不变头 m4s_list []*m4s_link_item//m4s列表 缓冲 } @@ -273,7 +275,7 @@ const ( ) var savestream = Savestream { - flv_stream:msgq.New(10),//队列最多保留10个关键帧间隔片 + stream:msgq.New(10),//队列最多保留10个关键帧间隔片 m4s_hls:8, } @@ -618,7 +620,7 @@ func Savestreamf(){ if offset,_ := out.Seek(0,1);offset == 0 { // fmt.Println(`添加头`,len(req.front)) //stream - savestream.flv_front = req.front + savestream.front = req.front out.Write(req.front) } @@ -657,7 +659,7 @@ func Savestreamf(){ for i:=0;i 1 { - // time.Sleep(time.Duration(3)*time.Second) - // } + // path = base_dir+path + w.Header().Set("Content-Type", "video/mp4") + w.WriteHeader(http.StatusOK) - res := savestream.hls_stream.b + flusher, flushSupport := w.(http.Flusher) + if flushSupport {flusher.Flush()} - if len(res) == 0 { - w.Header().Set("Retry-After", "1") - w.WriteHeader(http.StatusServiceUnavailable) - return - } + //写入hls头 + if _,err := w.Write(savestream.front);err != nil { + return + } else if flushSupport { + flusher.Flush() + } - //Server-Timing - w.Header().Set("Server-Timing", fmt.Sprintf("dur=%d", time.Since(start).Microseconds())) + cancel := make(chan struct{}) + + //hls切片 + savestream.stream.Pull_tag(map[string]func(interface{})(bool){ + `stream`:func(data interface{})(bool){ + if b,ok := data.([]byte);ok{ + if _,err := w.Write(b);err != nil { + close(cancel) + return true + } else if flushSupport { + flusher.Flush() + } + } + return false + }, + `close`:func(data interface{})(bool){ + close(cancel) + return true + }, + }) - if _,err := w.Write(res);err != nil { - flog.L(`E: `,err) - return + <- cancel + } else { + w.Header().Set("Cache-Control", "max-age=1") + w.Header().Set("Content-Type", "application/vnd.apple.mpegurl") + w.Header().Set("Last-Modified", savestream.hls_stream.t.Format(http.TimeFormat)) + + // //经常m4s下载速度赶不上,使用阻塞避免频繁获取列表带来的卡顿 + // if time.Now().Sub(savestream.hls_stream.t).Seconds() > 1 { + // time.Sleep(time.Duration(3)*time.Second) + // } + + res := savestream.hls_stream.b + + if len(res) == 0 { + w.Header().Set("Retry-After", "1") + w.WriteHeader(http.StatusServiceUnavailable) + return + } + + //Server-Timing + w.Header().Set("Server-Timing", fmt.Sprintf("dur=%d", time.Since(start).Microseconds())) + + if _,err := w.Write(res);err != nil { + flog.L(`E: `,err) + return + } } } } else if filepath.Ext(path) == `.m4s` { @@ -2069,35 +2176,12 @@ func init() { path = base_dir+path - var ( - buf []byte - cached bool - ) - - if b,ok := m4s_cache.Load(path);!ok{ - f,err := os.OpenFile(path,os.O_RDONLY,0644) - if err != nil { - flog.L(`E: `,err); - return - } - defer f.Close() + buf,cached,e := get_m4s_cache(path) - if b,e := io.ReadAll(f);e != nil { - flog.L(`E: `,e) - w.Header().Set("Retry-After", "1") - w.WriteHeader(http.StatusServiceUnavailable) - return - } else { - buf = b - m4s_cache.Store(path,buf) - go func(){//移除 - time.Sleep(time.Second*time.Duration(savestream.m4s_hls+1)) - m4s_cache.Delete(path) - }() - } - } else { - cached = true - buf,_ = b.([]byte) + if e != nil { + w.Header().Set("Retry-After", "1") + w.WriteHeader(http.StatusServiceUnavailable) + return } if len(buf) == 0 { @@ -2117,6 +2201,37 @@ func init() { } } else { w.Header().Set("Server", "file") + if r.URL.Query().Get("type") == "mp4" {//hls拼合 + dir := base_dir+filepath.Dir(path)+"/" + if !p.Checkfile().IsExist(dir+`0.m3u8`) { + w.WriteHeader(http.StatusNotFound) + return + } + var m4slist []string + if e := filepath.WalkDir(dir, func(path string, info fs.DirEntry, err error) error { + if err != nil {return err} + if filepath.Ext(info.Name()) == ".m4s" { + m4slist = append(m4slist, path) + } + return nil + });e != nil { + flog.L(`E: `,e); + w.WriteHeader(http.StatusServiceUnavailable) + return + } + m4slist = append(m4slist[len(m4slist)-1:], m4slist[:len(m4slist)-1]...) + for _,v := range m4slist { + f,err := os.OpenFile(v,os.O_RDONLY,0644) + if err != nil { + w.WriteHeader(http.StatusServiceUnavailable) + flog.L(`E: `,err) + return + } + io.Copy(w, f) + f.Close() + } + return + } http.FileServer(http.Dir(base_dir)).ServeHTTP(w,r) } } @@ -2153,6 +2268,9 @@ func init() { w.WriteHeader(http.StatusServiceUnavailable) return } + if r.URL.RawQuery != "" { + u.RawQuery = r.URL.RawQuery + } // r.URL = // root(w, r) w.Header().Set("Location", r.URL.ResolveReference(u).String()) -- 2.39.2