From c0e3ba2a8d5b325f862cf45eae7ea9be547f7504 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sun, 9 Apr 2023 19:18:28 +0200 Subject: [PATCH] add new example client-read-format-mpeg4audio-save-to-disk (#235) --- README.md | 1 + .../main.go | 10 +- .../main.go | 14 +-- .../mpegtsmuxer.go | 16 ++-- examples/client-read-format-h264/main.go | 10 +- examples/client-read-format-h265/main.go | 8 +- .../main.go | 84 +++++++++++++++++ .../mpegtsmuxer.go | 91 +++++++++++++++++++ examples/server-h264-save-to-disk/main.go | 4 +- 9 files changed, 207 insertions(+), 31 deletions(-) create mode 100644 examples/client-read-format-mpeg4audio-save-to-disk/main.go create mode 100644 examples/client-read-format-mpeg4audio-save-to-disk/mpegtsmuxer.go diff --git a/README.md b/README.md index 47be56e4..5b6bb7a8 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ Features: * [client-read-format-lpcm](examples/client-read-format-lpcm/main.go) * [client-read-format-mjpeg](examples/client-read-format-mjpeg/main.go) * [client-read-format-mpeg4audio](examples/client-read-format-mpeg4audio/main.go) +* [client-read-format-mpeg4audio-save-to-disk](examples/client-read-format-mpeg4audio-save-to-disk/main.go) * [client-read-format-opus](examples/client-read-format-opus/main.go) * [client-read-format-vp8](examples/client-read-format-vp8/main.go) * [client-read-format-vp9](examples/client-read-format-vp9/main.go) diff --git a/examples/client-read-format-h264-convert-to-jpeg/main.go b/examples/client-read-format-h264-convert-to-jpeg/main.go index b67dce6d..d55d746a 100644 --- a/examples/client-read-format-h264-convert-to-jpeg/main.go +++ b/examples/client-read-format-h264-convert-to-jpeg/main.go @@ -70,10 +70,10 @@ func main() { panic("media not found") } - // setup RTP/H264->H264 decoder + // setup RTP/H264 -> H264 decoder rtpDec := forma.CreateDecoder() - // setup H264->raw frames decoder + // setup H264 -> raw frames decoder h264RawDec, err := newH264Decoder() if err != nil { panic(err) @@ -97,8 +97,8 @@ func main() { // called when a RTP packet arrives saveCount := 0 c.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) { - // extract NALUs from RTP packets - nalus, _, err := rtpDec.Decode(pkt) + // extract access units from RTP packets + au, _, err := rtpDec.Decode(pkt) if err != nil { if err != rtph264.ErrNonStartingPacketAndNoPrevious && err != rtph264.ErrMorePacketsNeeded { log.Printf("ERR: %v", err) @@ -106,7 +106,7 @@ func main() { return } - for _, nalu := range nalus { + for _, nalu := range au { // convert NALUs into RGBA frames img, err := h264RawDec.decode(nalu) if err != nil { diff --git a/examples/client-read-format-h264-save-to-disk/main.go b/examples/client-read-format-h264-save-to-disk/main.go index 20dbe70b..42e18428 100644 --- a/examples/client-read-format-h264-save-to-disk/main.go +++ b/examples/client-read-format-h264-save-to-disk/main.go @@ -13,7 +13,7 @@ import ( // This example shows how to // 1. connect to a RTSP server // 2. check if there's a H264 media -// 3. save the content of the H264 media into a file in MPEG-TS format +// 3. save the content of the media into a file in MPEG-TS format func main() { c := gortsplib.Client{} @@ -44,10 +44,10 @@ func main() { panic("media not found") } - // setup RTP/H264->H264 decoder + // setup RTP/H264 -> H264 decoder rtpDec := forma.CreateDecoder() - // setup H264->MPEGTS muxer + // setup H264 -> MPEG-TS muxer mpegtsMuxer, err := newMPEGTSMuxer(forma.SPS, forma.PPS) if err != nil { panic(err) @@ -61,9 +61,9 @@ func main() { // called when a RTP packet arrives c.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) { - // extract NALUs from RTP packets + // extract access unit from RTP packets // DecodeUntilMarker is necessary for the DTS extractor to work - nalus, pts, err := rtpDec.DecodeUntilMarker(pkt) + au, pts, err := rtpDec.DecodeUntilMarker(pkt) if err != nil { if err != rtph264.ErrNonStartingPacketAndNoPrevious && err != rtph264.ErrMorePacketsNeeded { log.Printf("ERR: %v", err) @@ -71,8 +71,8 @@ func main() { return } - // encode H264 NALUs into MPEG-TS - mpegtsMuxer.encode(nalus, pts) + // encode the access unit into MPEG-TS + mpegtsMuxer.encode(au, pts) }) // start playing diff --git a/examples/client-read-format-h264-save-to-disk/mpegtsmuxer.go b/examples/client-read-format-h264-save-to-disk/mpegtsmuxer.go index 3fd5a965..91a45c18 100644 --- a/examples/client-read-format-h264-save-to-disk/mpegtsmuxer.go +++ b/examples/client-read-format-h264-save-to-disk/mpegtsmuxer.go @@ -54,8 +54,8 @@ func (e *mpegtsMuxer) close() { e.f.Close() } -// encode encodes H264 NALUs into MPEG-TS. -func (e *mpegtsMuxer) encode(nalus [][]byte, pts time.Duration) error { +// encode encodes a H264 access unit into MPEG-TS. +func (e *mpegtsMuxer) encode(au [][]byte, pts time.Duration) error { // prepend an AUD. This is required by some players filteredNALUs := [][]byte{ {byte(h264.NALUTypeAccessUnitDelimiter), 240}, @@ -64,7 +64,7 @@ func (e *mpegtsMuxer) encode(nalus [][]byte, pts time.Duration) error { nonIDRPresent := false idrPresent := false - for _, nalu := range nalus { + for _, nalu := range au { typ := h264.NALUType(nalu[0] & 0x1F) switch typ { case h264.NALUTypeSPS: @@ -88,7 +88,7 @@ func (e *mpegtsMuxer) encode(nalus [][]byte, pts time.Duration) error { filteredNALUs = append(filteredNALUs, nalu) } - nalus = filteredNALUs + au = filteredNALUs if !nonIDRPresent && !idrPresent { return nil @@ -96,7 +96,7 @@ func (e *mpegtsMuxer) encode(nalus [][]byte, pts time.Duration) error { // add SPS and PPS before every group that contains an IDR if idrPresent { - nalus = append([][]byte{e.sps, e.pps}, nalus...) + au = append([][]byte{e.sps, e.pps}, au...) } var dts time.Duration @@ -111,7 +111,7 @@ func (e *mpegtsMuxer) encode(nalus [][]byte, pts time.Duration) error { e.dtsExtractor = h264.NewDTSExtractor() var err error - dts, err = e.dtsExtractor.Extract(nalus, pts) + dts, err = e.dtsExtractor.Extract(au, pts) if err != nil { return err } @@ -122,7 +122,7 @@ func (e *mpegtsMuxer) encode(nalus [][]byte, pts time.Duration) error { } else { var err error - dts, err = e.dtsExtractor.Extract(nalus, pts) + dts, err = e.dtsExtractor.Extract(au, pts) if err != nil { return err } @@ -145,7 +145,7 @@ func (e *mpegtsMuxer) encode(nalus [][]byte, pts time.Duration) error { } // encode into Annex-B - annexb, err := h264.AnnexBMarshal(nalus) + annexb, err := h264.AnnexBMarshal(au) if err != nil { return err } diff --git a/examples/client-read-format-h264/main.go b/examples/client-read-format-h264/main.go index 7056b00d..0f80fb06 100644 --- a/examples/client-read-format-h264/main.go +++ b/examples/client-read-format-h264/main.go @@ -47,10 +47,10 @@ func main() { panic("media not found") } - // setup RTP/H264->H264 decoder + // setup RTP/H264 -> H264 decoder rtpDec := forma.CreateDecoder() - // setup H264->raw frames decoder + // setup H264 -> raw frames decoder h264RawDec, err := newH264Decoder() if err != nil { panic(err) @@ -73,8 +73,8 @@ func main() { // called when a RTP packet arrives c.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) { - // extract NALUs from RTP packets - nalus, pts, err := rtpDec.Decode(pkt) + // extract access units from RTP packets + au, pts, err := rtpDec.Decode(pkt) if err != nil { if err != rtph264.ErrNonStartingPacketAndNoPrevious && err != rtph264.ErrMorePacketsNeeded { log.Printf("ERR: %v", err) @@ -82,7 +82,7 @@ func main() { return } - for _, nalu := range nalus { + for _, nalu := range au { // convert NALUs into RGBA frames img, err := h264RawDec.decode(nalu) if err != nil { diff --git a/examples/client-read-format-h265/main.go b/examples/client-read-format-h265/main.go index 16334323..d409bacb 100644 --- a/examples/client-read-format-h265/main.go +++ b/examples/client-read-format-h265/main.go @@ -44,7 +44,7 @@ func main() { panic("media not found") } - // setup RTP/H265->H265 decoder + // setup RTP/H265 -> H265 decoder rtpDec := forma.CreateDecoder() // setup a single media @@ -55,8 +55,8 @@ func main() { // called when a RTP packet arrives c.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) { - // extract NALUs from RTP packets - nalus, pts, err := rtpDec.Decode(pkt) + // extract access units from RTP packets + au, pts, err := rtpDec.Decode(pkt) if err != nil { if err != rtph265.ErrNonStartingPacketAndNoPrevious && err != rtph265.ErrMorePacketsNeeded { log.Printf("ERR: %v", err) @@ -64,7 +64,7 @@ func main() { return } - for _, nalu := range nalus { + for _, nalu := range au { log.Printf("received NALU with PTS %v and size %d\n", pts, len(nalu)) } }) diff --git a/examples/client-read-format-mpeg4audio-save-to-disk/main.go b/examples/client-read-format-mpeg4audio-save-to-disk/main.go new file mode 100644 index 00000000..825dc044 --- /dev/null +++ b/examples/client-read-format-mpeg4audio-save-to-disk/main.go @@ -0,0 +1,84 @@ +package main + +import ( + "log" + + "github.com/bluenviron/gortsplib/v3" + "github.com/bluenviron/gortsplib/v3/pkg/formats" + "github.com/bluenviron/gortsplib/v3/pkg/url" + "github.com/pion/rtp" +) + +// This example shows how to +// 1. connect to a RTSP server +// 2. check if there's an MPEG4-audio media +// 3. save the content of the media into a file in MPEG-TS format + +func main() { + c := gortsplib.Client{} + + // parse URL + u, err := url.Parse("rtsp://localhost:8554/mystream") + if err != nil { + panic(err) + } + + // connect to the server + err = c.Start(u.Scheme, u.Host) + if err != nil { + panic(err) + } + defer c.Close() + + // find published medias + medias, baseURL, _, err := c.Describe(u) + if err != nil { + panic(err) + } + + // find the MPEG4-audio media and format + var forma *formats.MPEG4Audio + medi := medias.FindFormat(&forma) + if medi == nil { + panic("media not found") + } + + // setup RTP/MPEG4-audio -> MPEG4-audio decoder + rtpDec := forma.CreateDecoder() + + // setup MPEG4-audio -> MPEG-TS muxer + mpegtsMuxer, err := newMPEGTSMuxer(forma.Config) + if err != nil { + panic(err) + } + + // setup a single media + _, err = c.Setup(medi, baseURL, 0, 0) + if err != nil { + panic(err) + } + + // called when a RTP packet arrives + c.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) { + // extract access units from RTP packets + aus, pts, err := rtpDec.Decode(pkt) + if err != nil { + log.Printf("ERR: %v", err) + return + } + + for _, au := range aus { + // encode the access unit into MPEG-TS + mpegtsMuxer.encode(au, pts) + } + }) + + // start playing + _, err = c.Play(nil) + if err != nil { + panic(err) + } + + // wait until a fatal error + panic(c.Wait()) +} diff --git a/examples/client-read-format-mpeg4audio-save-to-disk/mpegtsmuxer.go b/examples/client-read-format-mpeg4audio-save-to-disk/mpegtsmuxer.go new file mode 100644 index 00000000..a9339536 --- /dev/null +++ b/examples/client-read-format-mpeg4audio-save-to-disk/mpegtsmuxer.go @@ -0,0 +1,91 @@ +package main + +import ( + "bufio" + "context" + "log" + "os" + "time" + + "github.com/asticode/go-astits" + "github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio" +) + +// mpegtsMuxer allows to save a MPEG4-audio stream into a MPEG-TS file. +type mpegtsMuxer struct { + config *mpeg4audio.Config + f *os.File + b *bufio.Writer + mux *astits.Muxer +} + +// newMPEGTSMuxer allocates a mpegtsMuxer. +func newMPEGTSMuxer(config *mpeg4audio.Config) (*mpegtsMuxer, error) { + f, err := os.Create("mystream.ts") + if err != nil { + return nil, err + } + b := bufio.NewWriter(f) + + mux := astits.NewMuxer(context.Background(), b) + mux.AddElementaryStream(astits.PMTElementaryStream{ + ElementaryPID: 257, + StreamType: astits.StreamTypeAACAudio, + }) + mux.SetPCRPID(257) + + return &mpegtsMuxer{ + config: config, + f: f, + b: b, + mux: mux, + }, nil +} + +// close closes all the mpegtsMuxer resources. +func (e *mpegtsMuxer) close() { + e.b.Flush() + e.f.Close() +} + +// encode encodes a MPEG4-audio access unit into MPEG-TS. +func (e *mpegtsMuxer) encode(au []byte, pts time.Duration) error { + // wrap access unit inside an ADTS packet + pkts := mpeg4audio.ADTSPackets{ + { + Type: e.config.Type, + SampleRate: e.config.SampleRate, + ChannelCount: e.config.ChannelCount, + AU: au, + }, + } + enc, err := pkts.Marshal() + if err != nil { + return err + } + + _, err = e.mux.WriteData(&astits.MuxerData{ + PID: 257, + AdaptationField: &astits.PacketAdaptationField{ + RandomAccessIndicator: true, + }, + PES: &astits.PESData{ + Header: &astits.PESHeader{ + OptionalHeader: &astits.PESOptionalHeader{ + MarkerBits: 2, + PTSDTSIndicator: astits.PTSDTSIndicatorOnlyPTS, + PTS: &astits.ClockReference{Base: int64(pts.Seconds() * 90000)}, + }, + PacketLength: uint16(len(enc) + 8), + StreamID: 192, // audio + }, + Data: enc, + }, + }) + if err != nil { + return err + } + + log.Println("wrote TS packet") + return nil +} diff --git a/examples/server-h264-save-to-disk/main.go b/examples/server-h264-save-to-disk/main.go index 53838e8f..56444ddb 100644 --- a/examples/server-h264-save-to-disk/main.go +++ b/examples/server-h264-save-to-disk/main.go @@ -75,10 +75,10 @@ func (sh *serverHandler) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) ( }, fmt.Errorf("H264 media not found") } - // setup RTP/H264->H264 decoder + // setup RTP/H264 -> H264 decoder rtpDec := forma.CreateDecoder() - // setup H264->MPEGTS muxer + // setup H264 -> MPEGTS muxer mpegtsMuxer, err := newMPEGTSMuxer(forma.SPS, forma.PPS) if err != nil { return &base.Response{