yapingcat / gomedia

golang library for rtmp, mpeg-ts,mpeg-ps,flv,mp4,ogg,rtsp
MIT License
400 stars 69 forks source link

example request: progressive mp4 stream to mepg-ts hls #6

Closed fastfading closed 2 years ago

fastfading commented 2 years ago

progressive.mp4 is a mp4 file on local disk or nfs mount. hls player try to play this file with hls protocol.

example server will read mp4, generate m3u8 and stream it in mpeg-ts format. reference https://www.jianshu.com/p/5eb817ccdd1f

yapingcat commented 2 years ago

我试试看,能不能按照这个要求写个example

yapingcat commented 2 years ago

I've uploaded this example,play mp4 with hls, please try to test. this cost so long ,I'm sorry for that

yapingcat commented 2 years ago

I'm just getting ready to develop fmp4, i hope i could finish it on August

fastfading commented 2 years ago

try mp4ff, https://github.com/edgeware/mp4ff I find this lib support fmp4 very well, and the mp4 support version is quite new

fastfading commented 2 years ago

I tried safari , it does not work , I put
http://127.0.0.1:19999/vod/xx.m3u8 in safari it download m3u8 and first ts, but not played

fastfading commented 2 years ago

VLC Report [0000000153e050b0] main libvlc: Running vlc with the default interface. Use 'cvlc' to use vlc without interface. [0000000153ec7a30] ts demux error: libdvbpsi error (PAT decoder): invalid section (section_syntax_indicator == 0) [0000000153ec7a30] ts demux error: libdvbpsi error (PSI decoder): TS discontinuity (received 2, expected 1) for PID 0 [0000000153ec7a30] ts demux error: libdvbpsi error (PAT decoder): invalid section (section_syntax_indicator == 0) [0000000153ec7a30] ts demux error: libdvbpsi error (PSI decoder): TS discontinuity (received 4, expected 3) for PID 0 [0000000153ec7a30] ts demux error: libdvbpsi error (PAT decoder): invalid section (section_syntax_indicator == 0) [0000000153ec7a30] ts demux error: libdvbpsi error (PSI decoder): TS discontinuity (received 6, expected 5) for PID 0

I am following example_demux_ts.go and example_mux_ts.go to do the remux recently. it report the same error.
TS format seems not right

yapingcat commented 2 years ago

please upload your test file.

yapingcat commented 2 years ago

sry, I can not connect to this website.

yapingcat commented 2 years ago

use ffplay to test ,and show me the log output of ffplay

fastfading commented 2 years ago

there is no error in ffplay ,ffplay plays well. but can not be played in quicktime vlc and safari use safari to connect mega.nz , refresh several times you could easy duplicate this case with any progressive mp4 (normal mp4)

yapingcat commented 2 years ago

please update to the latest version, and try to test again. some bugs about this issue has been fixed

fastfading commented 2 years ago

thanks it works well in play mp4 with hls,

but not well in remux which I did a little change base on example_demux_ts.go and example_mux_ts.go

here is the code. I am not sure where is wrong example_demux_ts.go.zip

here is the script , index.html , server.go to help duplicate this case.
all.tar.zip

Test Data could be any ts file
find . -name "*.ts" | head -10 | xargs -I % gomedia/example/example_demux_ts % 1

In this example, the code is quite simple, did nothing but demux the ts , and remux it with its original pts. check the "bDump" related code.

yapingcat commented 2 years ago
package main

import (
    "bytes"
    "fmt"
    "io/ioutil"
    "os"
    "path/filepath"

    "github.com/yapingcat/gomedia/codec"
    "github.com/yapingcat/gomedia/mpeg2"
)

func main() {
    args := os.Args
    tsfile := args[1]
    tsFd, err := os.Open(tsfile)
    if err != nil {
        fmt.Println(err)
        return
    }
    defer tsFd.Close()

    ts2file := `re` + filepath.Base(tsfile)
    ts2Fd, err := os.OpenFile(ts2file, os.O_CREATE|os.O_RDWR, 0666)
    if err != nil {
        fmt.Println(err)
        return
    }
    defer ts2Fd.Close()

    muxer := mpeg2.NewTSMuxer()
    muxer.OnPacket = func(pkg []byte) {
        fmt.Println("write packet")
        ts2Fd.Write(pkg)
    }
    pid_v := muxer.AddStream(mpeg2.TS_STREAM_H264)
    pid_a := muxer.AddStream(mpeg2.TS_STREAM_AAC)

    demuxer := mpeg2.NewTSDemuxer()
    demuxer.OnFrame = func(cid mpeg2.TS_STREAM_TYPE, frame []byte, pts uint64, dts uint64) {
        if cid == mpeg2.TS_STREAM_H264 {
            muxer.Write(pid_v, frame, pts, dts)
        } else if cid == mpeg2.TS_STREAM_AAC {
            muxer.Write(pid_a, frame, pts, dts)
        }
    }

    buf, _ := ioutil.ReadAll(tsFd)
    fmt.Printf("read %d size\n", len(buf))
    fmt.Println(demuxer.Input(bytes.NewReader(buf)))
    /*
       if ts file is large,please use bufio.NewReader
       demuxer.Input(bufio.NewReader(tsFd))
    */
}

please use above code to test

yapingcat commented 2 years ago

with this code, ts file can be played with QuickTime ,ffplay and vlc

yapingcat commented 2 years ago

on apple device, Aud nalu should been inserted to the beginning of the vcl frame automatically.but sps/pps/sei is also Access Unit Delimiter . so you need merge sps/pps/sei into vcl nalu (on most case,you should merge into I Frame).

fastfading commented 2 years ago

after fix vlc/safari play ts file failed most player plays well.

Firefox player seek very slow.

In android chrome/edge , video.js player when you do seek(拖动进度条) , there will be a weird behaviour .

in xiaomi edge player , there will be a very quick play video before seek to the right position (拖动后,视频快速播放,直到跳到正确的位置) in samsung chrome/edge player , there will be a image long ago, then jump to the right position.
(拖动后,先show 一个相对靠前位置的图片, 然后跳到正确位置)

yapingcat commented 2 years ago

Firefox 用 srs player 播放没问题 其他的场景,我身边没有这么多手机,暂时测试不了

yapingcat commented 2 years ago

我感觉不是码流的问题,是不是hls协议层面需要增加点东西,才能让seek比较自然

yapingcat commented 2 years ago

有和其他的 hls vod流 对比测试过吗 ?

yapingcat commented 2 years ago

按照最新版本,remux代码 已经更新

最新版本你只需要把ts demuxer 回调出来的帧数据写入到ts muxer即可

yapingcat commented 2 years ago

seek异常的问题,现在怎样了?

fastfading commented 2 years ago

可能是我数据源的问题, 我换了一个就好了。 这个ticket 可以关掉了

fastfading commented 2 years ago

每次切ts (func onTs) 之前 都重复的demuxer.ReadHead()
这里能不能做些优化, 前面m3u8直接记录 mp4切片偏移位置 下次就不用读header了。 直接切片

参考 https://www.jianshu.com/p/5eb817ccdd1f 这个适配端主要做的工作就是根据index文件和m3u8文件,计算出真实数据位置,然后向服务器发送Range请求,

yapingcat commented 2 years ago

仅仅通过”mp4切片偏移位置“ 这个信息没办法构建ts文件的,因为需要知道一个帧的大小和时间戳 时间戳和帧大小的信息都存在moov中,当前只能通过demuxer.ReadHead() 来解析moov。

如果要优化有两种方式

  1. 你可以增加一个接口,获取每个track 的 sample list(参见mp4track 中的 samplelist变量)。依据TS的http请求中的start和end来生成 TS切片文件,实际上是把SeekTime 这个接口的所做工作,由你自己来做。这样对于一个mp4文件来说,你只需要要保存他的samplelist就可以了,不需要每次ReadHead
  2. 第二种方法我比较推荐。 对于hls,一个ts/m3u8请求,你是无法知道这个请求到底是属于哪个客户端的。所以你需要用http协议的某些技术来指明这个请求来自于哪个客户端。解决这个问题之后,你可以为每个客户端保存一个mp4 demuxer 变量,不需要每次都调用ReadHead。 一般可以用http重定向或者http Cookie 这两种方法标识这个请求属于哪个客户端

最后,既然服务端要保存一些东西,你就需要去释放他,这个也是你需要考虑的

还有 ReadHead 现在是有什么性能瓶颈吗?

fastfading commented 2 years ago

如果文件放磁盘, nfs/samba 上 , 每次ReadHead 问题不大。 如果文件在s3 上, 每次只能range请求文件,每次读head就会有瓶颈。 我比较倾向 方案1, 为每个文件创建一个track samplist 结构, 放到内存里,并设置 ttl, 长时间不访问就删除。 第一次访问读到内存里, 后面人就从内存里获取。

方案2, 不理解为什么要为每个客户端 保存一个 demuxer 变量,我的理解是一个文件对不同客户端的索引结构是公用的。

yapingcat commented 2 years ago

方案2, 不理解为什么要为每个客户端 保存一个 demuxer 变量,我的理解是一个文件对不同客户端的索引结构是公用的。

demuxer 非线程安全。 内部有个read 索引,多个客户端共用同一个索引,会有问题

方案1

增加一个接口,获取demuxer 中的每个track 的samplelist变量就可以了 然后读到的每一个视频/音频 sample 你需要做一些处理 参考ReadPacket 接口 我感觉有点麻烦,你可以尝试写写看

yapingcat commented 2 years ago

我这边想到一个办法,可以一次性把moov读到内存当中,使用bytes.Reader,这样就可以避免多次io,只操作内存。 代价是内存中需要长期保存moov,在被点播的mp4比较多的时候对于内存大小有一定的要求

参考代码如下

package main

import (
    "bytes"
    "encoding/binary"
    "errors"
    "fmt"
    "io"
    "os"
    "strconv"

    "github.com/yapingcat/gomedia/mp4"
)

func mov_tag(tag [4]byte) uint32 {
    return binary.LittleEndian.Uint32(tag[:])
}

func main() {
    mp4FilePath := os.Args[1]
    newTime, err := strconv.Atoi(os.Args[2])
    if err != nil {
        fmt.Println(err)
        return
    }

    timeBuf := make([]byte, 4)
    binary.BigEndian.PutUint32(timeBuf, uint32(newTime))

    mp4Fd, err := os.OpenFile(mp4FilePath, os.O_CREATE|os.O_RDWR, 0666)
    if err != nil {
        fmt.Println(err)
        return
    }
    var moov []byte
Loop:
    for err == nil {
        basebox := mp4.BasicBox{}
        _, err = basebox.Decode(mp4Fd)
        if err != nil {
            break
        }
        if basebox.Size < mp4.BasicBoxLen {
            err = errors.New("mp4 Parser error")
            break
        }
        fmt.Println(string(basebox.Type[:]))
        tagName := mov_tag(basebox.Type)
        switch tagName {
        case mov_tag([4]byte{'m', 'o', 'o', 'v'}):
            moov = make([]byte, basebox.Size)
            mp4Fd.Seek(-1*mp4.BasicBoxLen, io.SeekCurrent)
            io.ReadFull(mp4Fd, moov)
            fmt.Println("Got moov box")
            break Loop
        default:
            _, err = mp4Fd.Seek(int64(basebox.Size)-mp4.BasicBoxLen, io.SeekCurrent)
        }
    }

    demuxer := mp4.CreateMp4Demuxer(bytes.NewReader(moov))
    if infos, err := demuxer.ReadHead(); err != nil && err != io.EOF {
        fmt.Println(err)
    } else {
        fmt.Printf("%+v\n", infos)
    }
    mp4info := demuxer.GetMp4Info()
    fmt.Printf("%+v\n", mp4info)
    mp4Fd.Seek(0, io.SeekStart)
    demuxer.RebindIo(mp4Fd)

    vfile, err := os.OpenFile("e.h264", os.O_CREATE|os.O_RDWR, 0666)
    if err != nil {
        fmt.Println(err)
        return
    }
    defer vfile.Close()
    afile, err := os.OpenFile("e.aac", os.O_CREATE|os.O_RDWR, 0666)
    if err != nil {
        fmt.Println(err)
        return
    }
    defer afile.Close()

    for {
        pkg, err := demuxer.ReadPacket()
        if err != nil {
            fmt.Println(err)
            break
        }
        fmt.Printf("track:%d,cid:%+v,pts:%d dts:%d\n", pkg.TrackId, pkg.Cid, pkg.Pts, pkg.Dts)
        if pkg.Cid == mp4.MP4_CODEC_H264 {
            vfile.Write(pkg.Data)
        } else if pkg.Cid == mp4.MP4_CODEC_AAC {
            afile.Write(pkg.Data)
        }
    }

    fmt.Println(err)
    //mp4Fd.Seek(int64(basebox.Size)-mp4.BasicBoxLen, io.SeekCurrent)
    return
}

demuxer 需要增加RebindIo接口

func (demuxer *MovDemuxer) RebindIo(r io.ReadSeeker) {
    demuxer.reader = r
}
yapingcat commented 2 years ago

对于一个mp4文件你只需要在内存中保存他的moov box 就可以

fastfading commented 2 years ago

请仔细看 https://www.jianshu.com/p/5eb817ccdd1f image image 是不是增加 seg_offset seg_size 就能解决这个问题

yapingcat commented 2 years ago

不能. seg_offset, seg_size的目的是让你能通过一次read 操作就把这个切片的所有sample从mp4文件中读取出来 但是这个还不够,你还需要知道在这个切片中每个sample的大小和时间戳信息。其实就是等于你需要知道mp4整个sample list。

这个samples_变量可能就是整个mp4文件的samplelist

wecom-temp-c814c00baa69beedbf7733af04114b5c
yapingcat commented 2 years ago

我想问一下,为什么不直接使用http + mp4文件点播的方式呢?

fastfading commented 2 years ago

我也想, security 不允许, 说 html5 不安全, 太容易下载。 太操蛋了。 业务需求,既要 web online playback , 又要web download。 所以存储下来只能是 progressive mp4 , 不能是fmp4,否则很多player , 编辑软件都不支持。 video tag 里面是 m3u8 用blob 字段隐藏

yapingcat commented 2 years ago

理解

fastfading commented 2 years ago

这个过程可以反过来吗? 用另外一个思路解决问题 我存一个 fmp4 和m3u8 用于 play back , download 的时候在内存中转换成 普通(progressive) mp4 https://github.com/edgeware/mp4ff/issues/162

yapingcat commented 2 years ago

这个可以。 但是目前我想到有两个问题 1.各个平台 各个浏览器 对与hls + fmp4 这种方式支持度怎么样? 2.使用fmp4 必然会有大量的小文件,你们的存储能够支持吗?

yapingcat commented 2 years ago

mp4demuxer.go.zip

我修改了代码,让ReadHead的时候 io操作的次数比较少。觉得可行,你可以fork gomedia,自己定制化修改

fastfading commented 2 years ago

fmp4 playback 这条路我们已经验证过了 各个平台浏览器都能支持, fmp4 可以是range 模式的, 从头到尾一个mp4 搞定, m3u8 文件里range 指定分片

我们不确定的是这个逆向转化过程是否可行 最简单的方式是,从s3 download 下来整个fmp4 , 然后ffmpeg 转换 mp4,然后让用户下载。 但这个过程太花时间, 最好有in memory 的边下,边转换过程

yapingcat commented 2 years ago

不行,只有在写入最后一帧之后才能 设置mdat box的size。只能mp4转完了,才能让用户下载

fastfading commented 2 years ago

我修改了代码,让ReadHead的时候 io操作的次数比较少。觉得可行,你可以fork gomedia,自己定制化修改

有没有办法(脚本) 我做下, performance 测试 模拟随机请求。

yapingcat commented 2 years ago

对随机性没要求,可以用脚本起多个ffmpeg 进程拉 hls流。用ffmpeg 切片按序下载。不是随机下载

yapingcat commented 2 years ago

如果你对码流数据不关心,简单写个http客户端就可以了,hls切片url按照一定规则生成,http测试客户端就按照这个规则生成url 随机下载切片就可以了