diff --git a/cmd/api/api.go b/cmd/api/api.go index 8df82847..31b9f2d6 100644 --- a/cmd/api/api.go +++ b/cmd/api/api.go @@ -36,8 +36,6 @@ func Init() { initStatic(cfg.Mod.StaticDir) initWS() - HandleFunc("/api/frame.mp4", frameHandler) - HandleFunc("/api/frame.raw", frameHandler) HandleFunc("/api/streams", streamsHandler) HandleFunc("/api/ws", apiWS) diff --git a/cmd/api/keyframe.go b/cmd/api/keyframe.go deleted file mode 100644 index d0a41ebf..00000000 --- a/cmd/api/keyframe.go +++ /dev/null @@ -1,40 +0,0 @@ -package api - -import ( - "github.com/AlexxIT/go2rtc/cmd/streams" - "github.com/AlexxIT/go2rtc/pkg/keyframe" - "net/http" - "strings" -) - -func frameHandler(w http.ResponseWriter, r *http.Request) { - src := r.URL.Query().Get("src") - stream := streams.Get(src) - if stream == nil { - return - } - - var ch = make(chan []byte) - - cons := new(keyframe.Consumer) - cons.IsMP4 = strings.HasSuffix(r.URL.Path, ".mp4") - cons.Listen(func(msg interface{}) { - switch msg.(type) { - case []byte: - ch <- msg.([]byte) - } - }) - - if err := stream.AddConsumer(cons); err != nil { - log.Warn().Err(err).Msg("[api.frame] add consumer") - return - } - - data := <-ch - - stream.RemoveConsumer(cons) - - if _, err := w.Write(data); err != nil { - log.Error().Err(err).Msg("[api.frame] write") - } -} diff --git a/cmd/mp4/mp4.go b/cmd/mp4/mp4.go new file mode 100644 index 00000000..c75fb5e8 --- /dev/null +++ b/cmd/mp4/mp4.go @@ -0,0 +1,129 @@ +package mp4 + +import ( + "github.com/AlexxIT/go2rtc/cmd/api" + "github.com/AlexxIT/go2rtc/cmd/app" + "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/pkg/mp4" + "github.com/rs/zerolog" + "net/http" + "strconv" + "strings" +) + +func Init() { + log = app.GetLogger("mp4") + + api.HandleWS(MsgTypeMSE, handlerWS) + + api.HandleFunc("/api/frame.mp4", handlerKeyframe) + api.HandleFunc("/api/stream.mp4", handlerMP4) +} + +var log zerolog.Logger + +func handlerKeyframe(w http.ResponseWriter, r *http.Request) { + if isChromeFirst(w, r) { + return + } + + src := r.URL.Query().Get("src") + stream := streams.Get(src) + if stream == nil { + return + } + + exit := make(chan []byte) + + cons := &mp4.Consumer{} + cons.Listen(func(msg interface{}) { + switch msg := msg.(type) { + case []byte: + exit <- msg + } + }) + + if err := stream.AddConsumer(cons); err != nil { + log.Error().Err(err).Msg("[api.keyframe] add consumer") + return + } + + w.Header().Set("Content-Type", cons.MimeType()) + + data := cons.Init() + data = append(data, <-exit...) + + stream.RemoveConsumer(cons) + + // Apple Safari won't show frame without length + w.Header().Set("Content-Length", strconv.Itoa(len(data))) + + if _, err := w.Write(data); err != nil { + log.Error().Err(err).Msg("[api.keyframe] add consumer") + } +} + +func handlerMP4(w http.ResponseWriter, r *http.Request) { + if isChromeFirst(w, r) || isSafari(w, r) { + return + } + + log.Trace().Msgf("[api.mp4] %+v", r) + + src := r.URL.Query().Get("src") + stream := streams.Get(src) + if stream == nil { + return + } + + exit := make(chan struct{}) + + cons := &mp4.Consumer{} + cons.Listen(func(msg interface{}) { + switch msg := msg.(type) { + case []byte: + if _, err := w.Write(msg); err != nil { + exit <- struct{}{} + } + } + }) + + if err := stream.AddConsumer(cons); err != nil { + log.Error().Err(err).Msg("[api.mp4] add consumer") + return + } + + defer stream.RemoveConsumer(cons) + + w.Header().Set("Content-Type", cons.MimeType()) + + data := cons.Init() + if _, err := w.Write(data); err != nil { + log.Error().Err(err).Msg("[api.mp4] write") + return + } + + <-exit + + log.Trace().Msg("[api.mp4] close") +} + +func isChromeFirst(w http.ResponseWriter, r *http.Request) bool { + // Chrome 105 does two requests: without Range and with `Range: bytes=0-` + if strings.Contains(r.UserAgent(), " Chrome/") { + if r.Header.Values("Range") == nil { + w.Header().Set("Content-Type", "video/mp4") + w.WriteHeader(http.StatusOK) + return true + } + } + return false +} + +func isSafari(w http.ResponseWriter, r *http.Request) bool { + if r.Header.Get("Range") == "bytes=0-1" { + handlerKeyframe(w, r) + return true + } + return false +} diff --git a/cmd/mse/mse.go b/cmd/mp4/mse.go similarity index 64% rename from cmd/mse/mse.go rename to cmd/mp4/mse.go index 8ae54b2a..47039459 100644 --- a/cmd/mse/mse.go +++ b/cmd/mp4/mse.go @@ -1,35 +1,34 @@ -package mse +package mp4 import ( "github.com/AlexxIT/go2rtc/cmd/api" "github.com/AlexxIT/go2rtc/cmd/streams" - "github.com/AlexxIT/go2rtc/pkg/mse" + "github.com/AlexxIT/go2rtc/pkg/mp4" "github.com/AlexxIT/go2rtc/pkg/streamer" - "github.com/rs/zerolog/log" ) -func Init() { - api.HandleWS("mse", handler) -} +const MsgTypeMSE = "mse" // fMP4 -func handler(ctx *api.Context, msg *streamer.Message) { +func handlerWS(ctx *api.Context, msg *streamer.Message) { src := ctx.Request.URL.Query().Get("src") stream := streams.Get(src) if stream == nil { return } - cons := new(mse.Consumer) + cons := &mp4.Consumer{} cons.UserAgent = ctx.Request.UserAgent() cons.RemoteAddr = ctx.Request.RemoteAddr + cons.Listen(func(msg interface{}) { switch msg.(type) { case *streamer.Message, []byte: ctx.Write(msg) } }) + if err := stream.AddConsumer(cons); err != nil { - log.Warn().Err(err).Msg("[api.mse] Add consumer") + log.Warn().Err(err).Msg("[api.mse] add consumer") ctx.Error(err) return } @@ -38,5 +37,9 @@ func handler(ctx *api.Context, msg *streamer.Message) { stream.RemoveConsumer(cons) }) - cons.Init() + ctx.Write(&streamer.Message{ + Type: MsgTypeMSE, Value: cons.MimeType(), + }) + + ctx.Write(cons.Init()) } diff --git a/main.go b/main.go index 860463f5..47d20b25 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,7 @@ import ( "github.com/AlexxIT/go2rtc/cmd/ffmpeg" "github.com/AlexxIT/go2rtc/cmd/hass" "github.com/AlexxIT/go2rtc/cmd/homekit" - "github.com/AlexxIT/go2rtc/cmd/mse" + "github.com/AlexxIT/go2rtc/cmd/mp4" "github.com/AlexxIT/go2rtc/cmd/ngrok" "github.com/AlexxIT/go2rtc/cmd/rtmp" "github.com/AlexxIT/go2rtc/cmd/rtsp" @@ -33,7 +33,7 @@ func main() { api.Init() // init HTTP API server webrtc.Init() - mse.Init() + mp4.Init() srtp.Init() homekit.Init() diff --git a/pkg/README.md b/pkg/README.md new file mode 100644 index 00000000..214677bc --- /dev/null +++ b/pkg/README.md @@ -0,0 +1,5 @@ +## Useful links + +- https://www.wowza.com/blog/streaming-protocols +- https://vimeo.com/blog/post/rtmp-stream/ +- https://sanjeev-pandey.medium.com/understanding-the-mpeg-4-moov-atom-pseudo-streaming-in-mp4-93935e1b9e9a \ No newline at end of file diff --git a/pkg/h264/helper.go b/pkg/h264/helper.go index da63ce94..1e623c46 100644 --- a/pkg/h264/helper.go +++ b/pkg/h264/helper.go @@ -17,6 +17,17 @@ func NALUType(b []byte) byte { return b[4] & 0x1F } +func IsKeyframe(b []byte) bool { + return NALUType(b) == NALUTypeIFrame +} + +func GetProfileLevelID(fmtp string) string { + if fmtp == "" { + return "" + } + return streamer.Between(fmtp, "profile-level-id=", ";") +} + func GetParameterSet(fmtp string) (sps, pps []byte) { if fmtp == "" { return diff --git a/pkg/keyframe/consumer.go b/pkg/keyframe/consumer.go deleted file mode 100644 index b721ddd3..00000000 --- a/pkg/keyframe/consumer.go +++ /dev/null @@ -1,72 +0,0 @@ -package keyframe - -import ( - "github.com/AlexxIT/go2rtc/pkg/h264" - "github.com/AlexxIT/go2rtc/pkg/mp4" - "github.com/AlexxIT/go2rtc/pkg/streamer" - "github.com/pion/rtp" -) - -var annexB = []byte{0, 0, 0, 1} - -type Consumer struct { - streamer.Element - IsMP4 bool -} - -func (k *Consumer) GetMedias() []*streamer.Media { - // support keyframe extraction only for one coded... - codec := streamer.NewCodec(streamer.CodecH264) - return []*streamer.Media{ - { - Kind: streamer.KindVideo, Direction: streamer.DirectionRecvonly, - Codecs: []*streamer.Codec{codec}, - }, - } -} - -func (k *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { - // sps and pps without AVC headers - sps, pps := h264.GetParameterSet(track.Codec.FmtpLine) - - push := func(packet *rtp.Packet) error { - // TODO: remove it, unnecessary - if packet.Version != h264.RTPPacketVersionAVC { - panic("wrong packet type") - } - - switch h264.NALUType(packet.Payload) { - case h264.NALUTypeSPS: - sps = packet.Payload[4:] // remove AVC header - case h264.NALUTypePPS: - pps = packet.Payload[4:] // remove AVC header - case h264.NALUTypeIFrame: - if sps == nil || pps == nil { - return nil - } - - var data []byte - - if k.IsMP4 { - data = mp4.MarshalMP4(sps, pps, packet.Payload) - } else { - data = append(data, annexB...) - data = append(data, sps...) - data = append(data, annexB...) - data = append(data, pps...) - data = append(data, annexB...) - data = append(data, packet.Payload[4:]...) - } - - k.Fire(data) - } - return nil - } - - if !h264.IsAVC(track.Codec) { - wrapper := h264.RTPDepay(track) - push = wrapper(push) - } - - return track.Bind(push) -} diff --git a/pkg/mp4/const.go b/pkg/mp4/const.go new file mode 100644 index 00000000..fb6eda86 --- /dev/null +++ b/pkg/mp4/const.go @@ -0,0 +1,94 @@ +package mp4 + +import ( + "encoding/binary" + "github.com/deepch/vdk/format/mp4/mp4io" + "time" +) + +var matrix = [9]int32{0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000} +var time0 = time.Date(1904, time.January, 1, 0, 0, 0, 0, time.UTC) + +func FTYP() []byte { + b := make([]byte, 0x18) + binary.BigEndian.PutUint32(b, 0x18) + copy(b[0x04:], "ftyp") + copy(b[0x08:], "iso5") + copy(b[0x10:], "iso5") + copy(b[0x14:], "avc1") + return b +} + +func MOOV() *mp4io.Movie { + return &mp4io.Movie{ + Header: &mp4io.MovieHeader{ + PreferredRate: 1, + PreferredVolume: 1, + Matrix: matrix, + NextTrackId: -1, + Duration: 0, + TimeScale: 1000, + CreateTime: time0, + ModifyTime: time0, + PreviewTime: time0, + PreviewDuration: time0, + PosterTime: time0, + SelectionTime: time0, + SelectionDuration: time0, + CurrentTime: time0, + }, + MovieExtend: &mp4io.MovieExtend{ + Tracks: []*mp4io.TrackExtend{ + { + TrackId: 1, + DefaultSampleDescIdx: 1, + DefaultSampleDuration: 40, + }, + }, + }, + } +} + +func TRAK() *mp4io.Track { + return &mp4io.Track{ + // trak > tkhd + Header: &mp4io.TrackHeader{ + TrackId: int32(1), // change me + Flags: 0x0007, // 7 ENABLED IN-MOVIE IN-PREVIEW + Duration: 0, // OK + Matrix: matrix, + CreateTime: time0, + ModifyTime: time0, + }, + // trak > mdia + Media: &mp4io.Media{ + // trak > mdia > mdhd + Header: &mp4io.MediaHeader{ + TimeScale: 1000, + Duration: 0, + Language: 0x55C4, + CreateTime: time0, + ModifyTime: time0, + }, + // trak > mdia > minf + Info: &mp4io.MediaInfo{ + // trak > mdia > minf > dinf + Data: &mp4io.DataInfo{ + Refer: &mp4io.DataRefer{ + Url: &mp4io.DataReferUrl{ + Flags: 0x000001, // self reference + }, + }, + }, + // trak > mdia > minf > stbl + Sample: &mp4io.SampleTable{ + SampleDesc: &mp4io.SampleDesc{}, + TimeToSample: &mp4io.TimeToSample{}, + SampleToChunk: &mp4io.SampleToChunk{}, + SampleSize: &mp4io.SampleSize{}, + ChunkOffset: &mp4io.ChunkOffset{}, + }, + }, + }, + } +} diff --git a/pkg/mp4/consumer.go b/pkg/mp4/consumer.go new file mode 100644 index 00000000..d51c4ebe --- /dev/null +++ b/pkg/mp4/consumer.go @@ -0,0 +1,107 @@ +package mp4 + +import ( + "encoding/json" + "fmt" + "github.com/AlexxIT/go2rtc/pkg/h264" + "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/pion/rtp" +) + +type Consumer struct { + streamer.Element + + UserAgent string + RemoteAddr string + + muxer *Muxer + codecs []*streamer.Codec + start bool + + send int +} + +func (c *Consumer) GetMedias() []*streamer.Media { + return []*streamer.Media{ + { + Kind: streamer.KindVideo, + Direction: streamer.DirectionRecvonly, + Codecs: []*streamer.Codec{ + {Name: streamer.CodecH264, ClockRate: 90000}, + }, + }, + //{ + // Kind: streamer.KindAudio, + // Direction: streamer.DirectionRecvonly, + // Codecs: []*streamer.Codec{ + // {Name: streamer.CodecAAC, ClockRate: 16000}, + // }, + //}, + } +} + +func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { + codec := track.Codec + switch codec.Name { + case streamer.CodecH264: + c.codecs = append(c.codecs, track.Codec) + + push := func(packet *rtp.Packet) error { + if packet.Version != h264.RTPPacketVersionAVC { + return nil + } + + switch h264.NALUType(packet.Payload) { + case h264.NALUTypeIFrame: + c.start = true + case h264.NALUTypePFrame: + if !c.start { + return nil + } + default: + return nil + } + + buf := c.muxer.Marshal(packet) + c.send += len(buf) + c.Fire(buf) + + return nil + } + + if !h264.IsAVC(codec) { + wrapper := h264.RTPDepay(track) + push = wrapper(push) + } + + return track.Bind(push) + } + + fmt.Printf("[rtmp] unsupported codec: %+v\n", track.Codec) + + return nil +} + +func (c *Consumer) MimeType() string { + return c.muxer.MimeType(c.codecs) +} + +func (c *Consumer) Init() []byte { + if c.muxer == nil { + c.muxer = &Muxer{} + } + return c.muxer.GetInit(c.codecs) +} + +// + +func (c *Consumer) MarshalJSON() ([]byte, error) { + v := map[string]interface{}{ + "type": "MP4 server consumer", + "send": c.send, + "remote_addr": c.RemoteAddr, + "user_agent": c.UserAgent, + } + + return json.Marshal(v) +} diff --git a/pkg/mp4/helpers.go b/pkg/mp4/helpers.go deleted file mode 100644 index d6b5cb97..00000000 --- a/pkg/mp4/helpers.go +++ /dev/null @@ -1,47 +0,0 @@ -package mp4 - -import ( - "errors" - "io" -) - -type MemoryWriter struct { - buf []byte - pos int -} - -func (m *MemoryWriter) Write(p []byte) (n int, err error) { - minCap := m.pos + len(p) - if minCap > cap(m.buf) { // Make sure buf has enough capacity: - buf2 := make([]byte, len(m.buf), minCap+len(p)) // add some extra - copy(buf2, m.buf) - m.buf = buf2 - } - if minCap > len(m.buf) { - m.buf = m.buf[:minCap] - } - copy(m.buf[m.pos:], p) - m.pos += len(p) - return len(p), nil -} - -func (m *MemoryWriter) Seek(offset int64, whence int) (int64, error) { - newPos, offs := 0, int(offset) - switch whence { - case io.SeekStart: - newPos = offs - case io.SeekCurrent: - newPos = m.pos + offs - case io.SeekEnd: - newPos = len(m.buf) + offs - } - if newPos < 0 { - return 0, errors.New("negative result pos") - } - m.pos = newPos - return int64(newPos), nil -} - -func (m *MemoryWriter) Bytes() []byte { - return m.buf -} diff --git a/pkg/mp4/muxer.go b/pkg/mp4/muxer.go index 6f6392ee..5629fc3e 100644 --- a/pkg/mp4/muxer.go +++ b/pkg/mp4/muxer.go @@ -1,37 +1,155 @@ package mp4 import ( - "github.com/deepch/vdk/av" + "encoding/binary" + "github.com/AlexxIT/go2rtc/pkg/h264" + "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/deepch/vdk/codec/h264parser" - "github.com/deepch/vdk/format/mp4" - "time" + "github.com/deepch/vdk/format/fmp4/fmp4io" + "github.com/deepch/vdk/format/mp4/mp4io" + "github.com/deepch/vdk/format/mp4f/mp4fio" + "github.com/pion/rtp" ) -func MarshalMP4(sps, pps, frame []byte) []byte { - writer := &MemoryWriter{} - muxer := mp4.NewMuxer(writer) - - stream, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps) - if err != nil { - panic(err) - } - - if err = muxer.WriteHeader([]av.CodecData{stream}); err != nil { - panic(err) - } - - pkt := av.Packet{ - CompositionTime: time.Millisecond, - IsKeyFrame: true, - Duration: time.Second, - Data: frame, - } - if err = muxer.WritePacket(pkt); err != nil { - panic(err) - } - if err = muxer.WriteTrailer(); err != nil { - panic(err) - } - - return writer.buf +type Muxer struct { + fragIndex uint32 + dts uint64 + pts uint32 + data []byte + total int +} + +func (m *Muxer) MimeType(codecs []*streamer.Codec) string { + s := `video/mp4; codecs="` + + for _, codec := range codecs { + switch codec.Name { + case streamer.CodecH264: + s += "avc1." + h264.GetProfileLevelID(codec.FmtpLine) + } + } + + return s + `"` +} + +func (m *Muxer) GetInit(codecs []*streamer.Codec) []byte { + moov := MOOV() + + for _, codec := range codecs { + switch codec.Name { + case streamer.CodecH264: + sps, pps := h264.GetParameterSet(codec.FmtpLine) + + // TODO: remove + codecData, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps) + if err != nil { + return nil + } + + width := codecData.Width() + height := codecData.Height() + + trak := TRAK() + trak.Media.Header.TimeScale = int32(codec.ClockRate) + trak.Header.TrackWidth = float64(width) + trak.Header.TrackHeight = float64(height) + + trak.Media.Info.Video = &mp4io.VideoMediaInfo{ + Flags: 0x000001, + } + trak.Media.Info.Sample.SampleDesc.AVC1Desc = &mp4io.AVC1Desc{ + DataRefIdx: 1, + HorizontalResolution: 72, + VorizontalResolution: 72, + Width: int16(width), + Height: int16(height), + FrameCount: 1, + Depth: 24, + ColorTableId: -1, + Conf: &mp4io.AVC1Conf{ + Data: codecData.AVCDecoderConfRecordBytes(), + }, + } + + trak.Media.Handler = &mp4io.HandlerRefer{ + SubType: [4]byte{'v', 'i', 'd', 'e'}, + Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0}, + } + + moov.Tracks = append(moov.Tracks, trak) + } + } + + data := make([]byte, moov.Len()) + moov.Marshal(data) + + return append(FTYP(), data...) +} + +func (m *Muxer) Rewind() { + m.dts = 0 + m.pts = 0 +} + +func (m *Muxer) Marshal(packet *rtp.Packet) []byte { + trackID := uint8(1) + + run := &mp4fio.TrackFragRun{ + Flags: 0x000b05, + FirstSampleFlags: uint32(fmp4io.SampleNoDependencies), + DataOffset: 0, + Entries: []mp4io.TrackFragRunEntry{}, + } + + moof := &mp4fio.MovieFrag{ + Header: &mp4fio.MovieFragHeader{ + Seqnum: m.fragIndex + 1, + }, + Tracks: []*mp4fio.TrackFrag{ + { + Header: &mp4fio.TrackFragHeader{ + Data: []byte{0x00, 0x02, 0x00, 0x20, 0x00, 0x00, 0x00, trackID, 0x01, 0x01, 0x00, 0x00}, + }, + DecodeTime: &mp4fio.TrackFragDecodeTime{ + Version: 1, + Flags: 0, + Time: m.dts, + }, + Run: run, + }, + }, + } + + entry := mp4io.TrackFragRunEntry{ + Duration: 90000, + Size: uint32(len(packet.Payload)), + } + + newTime := packet.Timestamp + if m.pts > 0 { + m.dts += uint64(newTime - m.pts) + } + m.pts = newTime + + // important before moof.Len() + run.Entries = append(run.Entries, entry) + + moofLen := moof.Len() + mdatLen := 8 + len(packet.Payload) + + // important after moof.Len() + run.DataOffset = uint32(moofLen + 8) + + buf := make([]byte, moofLen+mdatLen) + moof.Marshal(buf) + + binary.BigEndian.PutUint32(buf[moofLen:], uint32(mdatLen)) + copy(buf[moofLen+4:], "mdat") + copy(buf[moofLen+8:], packet.Payload) + + m.fragIndex++ + + m.total += moofLen + mdatLen + + return buf } diff --git a/pkg/mse/consumer.go b/pkg/mse/consumer.go deleted file mode 100644 index 77e5ab2b..00000000 --- a/pkg/mse/consumer.go +++ /dev/null @@ -1,131 +0,0 @@ -package mse - -import ( - "encoding/json" - "github.com/AlexxIT/go2rtc/pkg/h264" - "github.com/AlexxIT/go2rtc/pkg/streamer" - "github.com/deepch/vdk/av" - "github.com/deepch/vdk/codec/h264parser" - "github.com/deepch/vdk/format/mp4f" - "github.com/pion/rtp" - "time" -) - -const MsgTypeMSE = "mse" - -type Consumer struct { - streamer.Element - - UserAgent string - RemoteAddr string - - muxer *mp4f.Muxer - streams []av.CodecData - start bool - - send int -} - -func (c *Consumer) GetMedias() []*streamer.Media { - return []*streamer.Media{ - { - Kind: streamer.KindVideo, - Direction: streamer.DirectionRecvonly, - Codecs: []*streamer.Codec{ - {Name: streamer.CodecH264, ClockRate: 90000}, - }, - }, { - Kind: streamer.KindAudio, - Direction: streamer.DirectionRecvonly, - Codecs: []*streamer.Codec{ - {Name: streamer.CodecAAC, ClockRate: 16000}, - }, - }, - } -} - -func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { - codec := track.Codec - switch codec.Name { - case streamer.CodecH264: - idx := int8(len(c.streams)) - - sps, pps := h264.GetParameterSet(codec.FmtpLine) - stream, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps) - if err != nil { - return nil - } - c.streams = append(c.streams, stream) - - pkt := av.Packet{Idx: idx, CompositionTime: time.Millisecond} - - ts2time := time.Second / time.Duration(codec.ClockRate) - - push := func(packet *rtp.Packet) error { - if packet.Version != h264.RTPPacketVersionAVC { - return nil - } - - switch h264.NALUType(packet.Payload) { - case h264.NALUTypeIFrame: - c.start = true - pkt.IsKeyFrame = true - case h264.NALUTypePFrame: - if !c.start { - return nil - } - default: - return nil - } - - pkt.Data = packet.Payload - newTime := time.Duration(packet.Timestamp) * ts2time - if pkt.Time > 0 { - pkt.Duration = newTime - pkt.Time - } - pkt.Time = newTime - - for _, buf := range c.muxer.WritePacketV5(pkt) { - c.send += len(buf) - c.Fire(buf) - } - - return nil - } - - if !h264.IsAVC(codec) { - wrapper := h264.RTPDepay(track) - push = wrapper(push) - } - - return track.Bind(push) - } - - panic("unsupported codec") -} - -func (c *Consumer) Init() { - c.muxer = mp4f.NewMuxer(nil) - if err := c.muxer.WriteHeader(c.streams); err != nil { - return - } - - codecs, buf := c.muxer.GetInit(c.streams) - c.Fire(&streamer.Message{Type: MsgTypeMSE, Value: codecs}) - - c.send += len(buf) - c.Fire(buf) -} - -// - -func (c *Consumer) MarshalJSON() ([]byte, error) { - v := map[string]interface{}{ - "type": "MSE server consumer", - "send": c.send, - "remote_addr": c.RemoteAddr, - "user_agent": c.UserAgent, - } - - return json.Marshal(v) -} diff --git a/www/README.md b/www/README.md index 8063ede8..ba70e85c 100644 --- a/www/README.md +++ b/www/README.md @@ -51,4 +51,5 @@ pc.ontrack = ev => { ## Useful links - https://www.webrtc-experiment.com/DetectRTC/ -- https://divtable.com/table-styler/ \ No newline at end of file +- https://divtable.com/table-styler/ +- https://www.chromium.org/audio-video/ diff --git a/www/index.html b/www/index.html index f905753e..16c9e15c 100644 --- a/www/index.html +++ b/www/index.html @@ -66,7 +66,9 @@ const links = [ 'webrtc', 'mse', - 'frame.mp4', + // 'video', + 'mp4', + 'frame', 'info', ]; diff --git a/www/mse.html b/www/mse.html index 76de96d3..49446238 100644 --- a/www/mse.html +++ b/www/mse.html @@ -60,9 +60,7 @@ console.debug("ws.onmessage", data); if (data.type === "mse") { - sourceBuffer = mediaSource.addSourceBuffer( - `video/mp4; codecs="${data.value}"` - ); + sourceBuffer = mediaSource.addSourceBuffer(data.value); // important: segments supports TrackFragDecodeTime // sequence supports only TrackFragRunEntry Duration sourceBuffer.mode = "segments"; diff --git a/www/video.html b/www/video.html new file mode 100644 index 00000000..deccb78b --- /dev/null +++ b/www/video.html @@ -0,0 +1,53 @@ + + +
+ +