Added th opportunity to get more formats.
This commit is contained in:
parent
b26bcb9f69
commit
c59702fcff
@ -19,10 +19,10 @@ func main() {
|
|||||||
|
|
||||||
// Connect to each camera.
|
// Connect to each camera.
|
||||||
for _, link := range c {
|
for _, link := range c {
|
||||||
log.Printf("start recording on camera: %s\n", link)
|
log.Printf("process camera:\n %s\n", link)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
err = rtsp.RTSP(*directory, 60, link)
|
err = rtsp.StartWriter(*directory, 60, link)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("procRTSP function error for camera %s: %s", link, err.Error())
|
log.Printf("procRTSP function error for camera %s: %s", link, err.Error())
|
||||||
}
|
}
|
||||||
|
@ -6,16 +6,18 @@ require (
|
|||||||
git.insit.tech/sas/rtsp_proxy v0.0.0-20250310124520-82fa76149f4e
|
git.insit.tech/sas/rtsp_proxy v0.0.0-20250310124520-82fa76149f4e
|
||||||
github.com/Eyevinn/mp4ff v0.47.0
|
github.com/Eyevinn/mp4ff v0.47.0
|
||||||
github.com/bluenviron/gortsplib/v4 v4.12.3
|
github.com/bluenviron/gortsplib/v4 v4.12.3
|
||||||
|
github.com/bluenviron/mediacommon v1.14.0
|
||||||
github.com/bluenviron/mediacommon/v2 v2.0.0
|
github.com/bluenviron/mediacommon/v2 v2.0.0
|
||||||
github.com/gen2brain/aac-go v0.0.0-20230119102159-ef1e76509d21
|
github.com/gen2brain/aac-go v0.0.0-20230119102159-ef1e76509d21
|
||||||
|
github.com/hraban/opus v0.0.0-20230925203106-0188a62cb302
|
||||||
github.com/pion/rtp v1.8.12
|
github.com/pion/rtp v1.8.12
|
||||||
github.com/zaf/g711 v1.4.0
|
github.com/zaf/g711 v1.4.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/asticode/go-astikit v0.52.0 // indirect
|
github.com/asticode/go-astikit v0.52.0 // indirect
|
||||||
github.com/asticode/go-astits v1.13.0 // indirect
|
github.com/asticode/go-astits v1.13.0 // indirect
|
||||||
github.com/bluenviron/mediacommon v1.14.0 // indirect
|
|
||||||
github.com/cyub/ringbuffer v0.0.0-20221202135829-35445cc89929 // indirect
|
github.com/cyub/ringbuffer v0.0.0-20221202135829-35445cc89929 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/grafov/m3u8 v0.12.1 // indirect
|
github.com/grafov/m3u8 v0.12.1 // indirect
|
||||||
@ -24,5 +26,4 @@ require (
|
|||||||
github.com/pion/sdp/v3 v3.0.10 // indirect
|
github.com/pion/sdp/v3 v3.0.10 // indirect
|
||||||
golang.org/x/net v0.37.0 // indirect
|
golang.org/x/net v0.37.0 // indirect
|
||||||
golang.org/x/sys v0.31.0 // indirect
|
golang.org/x/sys v0.31.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
@ -26,6 +26,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/grafov/m3u8 v0.12.1 h1:DuP1uA1kvRRmGNAZ0m+ObLv1dvrfNO0TPx0c/enNk0s=
|
github.com/grafov/m3u8 v0.12.1 h1:DuP1uA1kvRRmGNAZ0m+ObLv1dvrfNO0TPx0c/enNk0s=
|
||||||
github.com/grafov/m3u8 v0.12.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
|
github.com/grafov/m3u8 v0.12.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
|
||||||
|
github.com/hraban/opus v0.0.0-20230925203106-0188a62cb302 h1:K7bmEmIesLcvCW0Ic2rCk6LtP5++nTnPmrO8mg5umlA=
|
||||||
|
github.com/hraban/opus v0.0.0-20230925203106-0188a62cb302/go.mod h1:YQQXrWHN3JEvCtw5ImyTCcPeU/ZLo/YMA+TpB64XdrU=
|
||||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||||
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||||
@ -56,6 +58,7 @@ golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
|||||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
@ -1,36 +1,36 @@
|
|||||||
#camera_1: rtsp://intercom-video-1.insit.ru/camera01-centr-pl_slavy
|
#camera_1: rtsp://intercom-video-1.insit.ru/camera01-centr-pl_slavy
|
||||||
#camera_2: rtsp://intercom-video-1.insit.ru/camera03-centr-administraciya
|
camera_2: rtsp://intercom-video-1.insit.ru/camera03-centr-administraciya
|
||||||
#camera_3: rtsp://intercom-video-1.insit.ru/camera08-centr-skver_kalinina
|
#camera_3: rtsp://intercom-video-1.insit.ru/camera08-centr-skver_kalinina
|
||||||
#camera_4: rtsp://intercom-video-1.insit.ru/camera04-centr-pobedy
|
camera_4: rtsp://intercom-video-1.insit.ru/camera04-centr-pobedy
|
||||||
#camera_5: rtsp://intercom-video-1.insit.ru/camera11-centr-dk_kirova
|
camera_5: rtsp://intercom-video-1.insit.ru/camera11-centr-dk_kirova
|
||||||
#camera_6: rtsp://intercom-video-1.insit.ru/camera13-centr-pobedy_sutyagina
|
camera_6: rtsp://intercom-video-1.insit.ru/camera13-centr-pobedy_sutyagina
|
||||||
#camera_7: rtsp://intercom-video-1.insit.ru/camera09-centr-nalogovaya
|
camera_7: rtsp://intercom-video-1.insit.ru/camera09-centr-nalogovaya
|
||||||
#camera_8: rtsp://intercom-video-1.insit.ru/camera59-centr-prktslavy
|
#camera_8: rtsp://intercom-video-1.insit.ru/camera59-centr-prktslavy
|
||||||
#camera_9: rtsp://intercom-video-1.insit.ru/camera10-centr-kommunisticheskiy_ilycha
|
camera_9: rtsp://intercom-video-1.insit.ru/camera10-centr-kommunisticheskiy_ilycha
|
||||||
#camera_10: rtsp://intercom-video-1.insit.ru/camera16-centr-sportschool2
|
camera_10: rtsp://intercom-video-1.insit.ru/camera16-centr-sportschool2
|
||||||
#camera_11: rtsp://intercom-video-1.insit.ru/camera40-center-kommunisticheskiy
|
camera_11: rtsp://intercom-video-1.insit.ru/camera40-center-kommunisticheskiy
|
||||||
#camera_12: rtsp://intercom-video-1.insit.ru/camera12-centr-skver_pavshih_geroev
|
camera_12: rtsp://intercom-video-1.insit.ru/camera12-centr-skver_pavshih_geroev
|
||||||
#camera_13: rtsp://intercom-video-1.insit.ru/camera39-center-kommunisticheskiy
|
camera_13: rtsp://intercom-video-1.insit.ru/camera39-center-kommunisticheskiy
|
||||||
#camera_14: rtsp://intercom-video-1.insit.ru/camera14-centr-stadion_himik
|
camera_14: rtsp://intercom-video-1.insit.ru/camera14-centr-stadion_himik
|
||||||
#camera_15: rtsp://intercom-video-1.insit.ru/camera41-center-kuznecova_borby
|
camera_15: rtsp://intercom-video-1.insit.ru/camera41-center-kuznecova_borby
|
||||||
#camera_16: rtsp://intercom-video-1.insit.ru/camera83-gorod-Kommunistichesky
|
camera_16: rtsp://intercom-video-1.insit.ru/camera83-gorod-Kommunistichesky
|
||||||
#camera_17: rtsp://intercom-video-1.insit.ru/camera54-centr-kirova-kalinina
|
camera_17: rtsp://intercom-video-1.insit.ru/camera54-centr-kirova-kalinina
|
||||||
#camera_18: rtsp://intercom-video-1.insit.ru/camera53-centr-pobedy_slavy
|
camera_18: rtsp://intercom-video-1.insit.ru/camera53-centr-pobedy_slavy
|
||||||
#camera_19: rtsp://intercom-video-1.insit.ru/camera44-center-skver_temnika
|
camera_19: rtsp://intercom-video-1.insit.ru/camera44-center-skver_temnika
|
||||||
#camera_20: rtsp://intercom-video-1.insit.ru/camera28-oktyabrskiy-severnaya23a
|
camera_20: rtsp://intercom-video-1.insit.ru/camera28-oktyabrskiy-severnaya23a
|
||||||
#camera_21: rtsp://intercom-video-1.insit.ru/dp-qamumnrlkizuypnetljzzkjqamdoti
|
camera_21: rtsp://intercom-video-1.insit.ru/dp-qamumnrlkizuypnetljzzkjqamdoti
|
||||||
#camera_22: rtsp://intercom-video-1.insit.ru/dp-ohusuxzcvzsnpzzvkpyhddnwxuyeyc
|
camera_22: rtsp://intercom-video-1.insit.ru/dp-ohusuxzcvzsnpzzvkpyhddnwxuyeyc
|
||||||
#camera_23: rtsp://intercom-video-1.insit.ru/dp-bflbwjulvgfzurmcpejklrfvqairns
|
camera_23: rtsp://intercom-video-1.insit.ru/dp-bflbwjulvgfzurmcpejklrfvqairns
|
||||||
#camera_24: rtsp://intercom-video-1.insit.ru/dp-ajiymmjyytokybrpganfcxlfyjcdbgezphn
|
camera_24: rtsp://intercom-video-1.insit.ru/dp-ajiymmjyytokybrpganfcxlfyjcdbgezphn
|
||||||
#camera_25: rtsp://intercom-video-2.insit.ru/dp-pobedi6a-ii-2125126423
|
camera_25: rtsp://intercom-video-2.insit.ru/dp-pobedi6a-ii-2125126423
|
||||||
#camera_26: rtsp://intercom-video-1.insit.ru/dp-pobedi11-ii-2108117729
|
camera_26: rtsp://intercom-video-1.insit.ru/dp-pobedi11-ii-2108117729
|
||||||
#camera_27: rtsp://intercom-video-2.insit.ru/dp-pobedi11-i-2108117197
|
camera_27: rtsp://intercom-video-2.insit.ru/dp-pobedi11-i-2108117197
|
||||||
#camera_28: rtsp://intercom-video-2.insit.ru/dp-nfhapwbfjpqkmaymfeipraxtzcpedk
|
camera_28: rtsp://intercom-video-2.insit.ru/dp-nfhapwbfjpqkmaymfeipraxtzcpedk
|
||||||
#camera_29: rtsp://intercom-video-1.insit.ru/dp-swcixufwlheiwwrcvsrbkmhqzqvbxz
|
camera_29: rtsp://intercom-video-1.insit.ru/dp-swcixufwlheiwwrcvsrbkmhqzqvbxz
|
||||||
#camera_30: rtsp://intercom-video-1.insit.ru/dp-nerrjszqrbhjvqmfxskunejafdiihj
|
#camera_30: rtsp://intercom-video-1.insit.ru/dp-nerrjszqrbhjvqmfxskunejafdiihj
|
||||||
#camera_31: rtsp://intercom-video-1.insit.ru/dp-aiwukyujwonohpjyzeniispqqullyr
|
camera_31: rtsp://intercom-video-1.insit.ru/dp-aiwukyujwonohpjyzeniispqqullyr
|
||||||
#camera_32: rtsp://intercom-video-1.insit.ru/dp-woxvkbynctgfbuztsalttgburbpvjf
|
camera_32: rtsp://intercom-video-1.insit.ru/dp-woxvkbynctgfbuztsalttgburbpvjf
|
||||||
#camera_33: rtsp://intercom-video-1.insit.ru/dp-fdzbasqehtptsuhxnjeqqnlrixfahcgvlcr
|
camera_33: rtsp://intercom-video-1.insit.ru/dp-fdzbasqehtptsuhxnjeqqnlrixfahcgvlcr
|
||||||
#camera_34: rtsp://intercom-video-1.insit.ru/dp-exyeqscyamrbkwkjifagouyprtsdoe
|
camera_34: rtsp://intercom-video-1.insit.ru/dp-exyeqscyamrbkwkjifagouyprtsdoe
|
||||||
#camera_35: rtsp://intercom-video-2.insit.ru/dp-sutyagina3a-iv-uujtwbsjekv
|
camera_35: rtsp://intercom-video-2.insit.ru/dp-sutyagina3a-iv-uujtwbsjekv
|
||||||
camera_36: rtsp://intercom-video-1.insit.ru/dp-wyshispseamhqmnhkqwkbarshnrvni
|
camera_36: rtsp://intercom-video-1.insit.ru/dp-wyshispseamhqmnhkqwkbarshnrvni
|
48
writer/internal/ingest/formats/aac.go
Normal file
48
writer/internal/ingest/formats/aac.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package formats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/bluenviron/gortsplib/v4"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/format"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FindAACFormat finds the AAC media and format.
|
||||||
|
func FindAACFormat(desc *description.Session) (*format.MPEG4Audio, *description.Media, error) {
|
||||||
|
var aacFormat *format.MPEG4Audio
|
||||||
|
aacMedia := desc.FindFormat(&aacFormat)
|
||||||
|
if aacMedia == nil {
|
||||||
|
return nil, nil, errors.New("media AAC not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return aacFormat, aacMedia, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessAAC processes AAC flow and returns PTS and AUS.
|
||||||
|
func ProcessAAC(
|
||||||
|
c *gortsplib.Client,
|
||||||
|
aacMedia *description.Media,
|
||||||
|
aacRTPDec *rtpmpeg4audio.Decoder,
|
||||||
|
pkt *rtp.Packet,
|
||||||
|
t string,
|
||||||
|
) (
|
||||||
|
int64,
|
||||||
|
[][]byte,
|
||||||
|
error) {
|
||||||
|
// Decode timestamp.
|
||||||
|
pts, ok := c.PacketPTS2(aacMedia, pkt)
|
||||||
|
if !ok {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: waiting for timestamp\n", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract access unit from RTP packets.
|
||||||
|
aus, err := aacRTPDec.Decode(pkt)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: decoding RTP packet error: %w", t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pts, aus, nil
|
||||||
|
}
|
71
writer/internal/ingest/formats/av1.go
Normal file
71
writer/internal/ingest/formats/av1.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package formats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"C"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/bluenviron/gortsplib/v4"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/format"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpav1"
|
||||||
|
"github.com/bluenviron/mediacommon/v2/pkg/codecs/av1"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
"image"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FindAV1Format finds the AV1 media and format.
|
||||||
|
func FindAV1Format(desc *description.Session) (*format.AV1, *description.Media, error) {
|
||||||
|
var av1Format *format.AV1
|
||||||
|
av1Media := desc.FindFormat(&av1Format)
|
||||||
|
if av1Media == nil {
|
||||||
|
return nil, nil, errors.New("media AV1 not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return av1Format, av1Media, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessAV1 processes AV1 flow and returns PTS and IMG.
|
||||||
|
func ProcessAV1(
|
||||||
|
c *gortsplib.Client,
|
||||||
|
av1Media *description.Media,
|
||||||
|
av1RTPDec *rtpav1.Decoder,
|
||||||
|
pkt *rtp.Packet,
|
||||||
|
av1Dec *AV1Decoder,
|
||||||
|
firstRandomReceived bool,
|
||||||
|
t string,
|
||||||
|
) (
|
||||||
|
int64,
|
||||||
|
*image.RGBA,
|
||||||
|
error) {
|
||||||
|
// Decode timestamp.
|
||||||
|
pts, ok := c.PacketPTS2(av1Media, pkt)
|
||||||
|
if !ok {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: waiting for timestamp\n", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract AV1 temporal units from RTP packets.
|
||||||
|
tu, err := av1RTPDec.Decode(pkt)
|
||||||
|
if err != rtpav1.ErrNonStartingPacketAndNoPrevious && err != rtpav1.ErrMorePacketsNeeded {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: decoding RTP packet error: %w", t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for a random access unit.
|
||||||
|
IsRandomAccess2, _ := av1.IsRandomAccess(tu)
|
||||||
|
if !firstRandomReceived && !IsRandomAccess2 {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: waiting for a random access unit\n", t)
|
||||||
|
}
|
||||||
|
firstRandomReceived = true
|
||||||
|
|
||||||
|
// Convert AV1 temporal units into RGBA frames.
|
||||||
|
img, err := av1Dec.Decode(tu)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: convert into RGBA frames error\n", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for a frame.
|
||||||
|
if img == nil {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: frame not found\n", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pts, img, nil
|
||||||
|
}
|
161
writer/internal/ingest/formats/av1_decoder.go
Normal file
161
writer/internal/ingest/formats/av1_decoder.go
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
package formats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"runtime"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/bluenviron/mediacommon/v2/pkg/codecs/av1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// #cgo pkg-config: libavcodec libavutil libswscale
|
||||||
|
// #include <libavcodec/avcodec.h>
|
||||||
|
// #include <libavutil/imgutils.h>
|
||||||
|
// #include <libswscale/swscale.h>
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
func frameDataAV1(frame *C.AVFrame) **C.uint8_t {
|
||||||
|
return (**C.uint8_t)(unsafe.Pointer(&frame.data[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func frameLineSizeAV1(frame *C.AVFrame) *C.int {
|
||||||
|
return (*C.int)(unsafe.Pointer(&frame.linesize[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AV1Decoder is a wrapper around FFmpeg's AV1 decoder.
|
||||||
|
type AV1Decoder struct {
|
||||||
|
codecCtx *C.AVCodecContext
|
||||||
|
yuv420Frame *C.AVFrame
|
||||||
|
rgbaFrame *C.AVFrame
|
||||||
|
rgbaFramePtr []uint8
|
||||||
|
swsCtx *C.struct_SwsContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize initializes a AV1Decoder.
|
||||||
|
func (d *AV1Decoder) Initialize() error {
|
||||||
|
codec := C.avcodec_find_decoder(C.AV_CODEC_ID_AV1)
|
||||||
|
if codec == nil {
|
||||||
|
return fmt.Errorf("avcodec_find_decoder() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
d.codecCtx = C.avcodec_alloc_context3(codec)
|
||||||
|
if d.codecCtx == nil {
|
||||||
|
return fmt.Errorf("avcodec_alloc_context3() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
res := C.avcodec_open2(d.codecCtx, codec, nil)
|
||||||
|
if res < 0 {
|
||||||
|
C.avcodec_close(d.codecCtx)
|
||||||
|
return fmt.Errorf("avcodec_open2() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
d.yuv420Frame = C.av_frame_alloc()
|
||||||
|
if d.yuv420Frame == nil {
|
||||||
|
C.avcodec_close(d.codecCtx)
|
||||||
|
return fmt.Errorf("av_frame_alloc() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the decoder.
|
||||||
|
func (d *AV1Decoder) Close() {
|
||||||
|
if d.swsCtx != nil {
|
||||||
|
C.sws_freeContext(d.swsCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.rgbaFrame != nil {
|
||||||
|
C.av_frame_free(&d.rgbaFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
C.av_frame_free(&d.yuv420Frame)
|
||||||
|
C.avcodec_close(d.codecCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *AV1Decoder) reinitDynamicStuff() error {
|
||||||
|
if d.swsCtx != nil {
|
||||||
|
C.sws_freeContext(d.swsCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.rgbaFrame != nil {
|
||||||
|
C.av_frame_free(&d.rgbaFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.rgbaFrame = C.av_frame_alloc()
|
||||||
|
if d.rgbaFrame == nil {
|
||||||
|
return fmt.Errorf("av_frame_alloc() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
d.rgbaFrame.format = C.AV_PIX_FMT_RGBA
|
||||||
|
d.rgbaFrame.width = d.yuv420Frame.width
|
||||||
|
d.rgbaFrame.height = d.yuv420Frame.height
|
||||||
|
d.rgbaFrame.color_range = C.AVCOL_RANGE_JPEG
|
||||||
|
|
||||||
|
res := C.av_frame_get_buffer(d.rgbaFrame, 1)
|
||||||
|
if res < 0 {
|
||||||
|
return fmt.Errorf("av_frame_get_buffer() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
d.swsCtx = C.sws_getContext(d.yuv420Frame.width, d.yuv420Frame.height, int32(d.yuv420Frame.format),
|
||||||
|
d.rgbaFrame.width, d.rgbaFrame.height, (int32)(d.rgbaFrame.format), C.SWS_BILINEAR, nil, nil, nil)
|
||||||
|
if d.swsCtx == nil {
|
||||||
|
return fmt.Errorf("sws_getContext() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
rgbaFrameSize := C.av_image_get_buffer_size((int32)(d.rgbaFrame.format), d.rgbaFrame.width, d.rgbaFrame.height, 1)
|
||||||
|
d.rgbaFramePtr = (*[1 << 30]uint8)(unsafe.Pointer(d.rgbaFrame.data[0]))[:rgbaFrameSize:rgbaFrameSize]
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode decodes a RGBA image from AV1.
|
||||||
|
func (d *AV1Decoder) Decode(tu [][]byte) (*image.RGBA, error) {
|
||||||
|
// encode temporal unit into bytestream
|
||||||
|
bs, err := av1.Bitstream(tu).Marshal()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// send temporal unit to decoder
|
||||||
|
var pkt C.AVPacket
|
||||||
|
ptr := &bs[0]
|
||||||
|
var p runtime.Pinner
|
||||||
|
p.Pin(ptr)
|
||||||
|
pkt.data = (*C.uint8_t)(ptr)
|
||||||
|
pkt.size = (C.int)(len(bs))
|
||||||
|
res := C.avcodec_send_packet(d.codecCtx, &pkt)
|
||||||
|
p.Unpin()
|
||||||
|
if res < 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// receive frame if available
|
||||||
|
res = C.avcodec_receive_frame(d.codecCtx, d.yuv420Frame)
|
||||||
|
if res < 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// if frame size has changed, allocate needed objects
|
||||||
|
if d.rgbaFrame == nil || d.rgbaFrame.width != d.yuv420Frame.width || d.rgbaFrame.height != d.yuv420Frame.height {
|
||||||
|
err := d.reinitDynamicStuff()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert color space from YUV420 to RGBA
|
||||||
|
res = C.sws_scale(d.swsCtx, frameDataAV1(d.yuv420Frame), frameLineSizeAV1(d.yuv420Frame),
|
||||||
|
0, d.yuv420Frame.height, frameDataAV1(d.rgbaFrame), frameLineSizeAV1(d.rgbaFrame))
|
||||||
|
if res < 0 {
|
||||||
|
return nil, fmt.Errorf("sws_scale() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// embed frame into an image.RGBA
|
||||||
|
return &image.RGBA{
|
||||||
|
Pix: d.rgbaFramePtr,
|
||||||
|
Stride: 4 * (int)(d.rgbaFrame.width),
|
||||||
|
Rect: image.Rectangle{
|
||||||
|
Max: image.Point{(int)(d.rgbaFrame.width), (int)(d.rgbaFrame.height)},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package rtsp
|
package formats
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@ -8,11 +8,10 @@ import (
|
|||||||
"github.com/bluenviron/gortsplib/v4/pkg/format"
|
"github.com/bluenviron/gortsplib/v4/pkg/format"
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/format/rtplpcm"
|
"github.com/bluenviron/gortsplib/v4/pkg/format/rtplpcm"
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
"log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CheckG711Format finds the G711 media and format.
|
// FindG711Format finds the G711 media and format.
|
||||||
func CheckG711Format(desc *description.Session) (*format.G711, *description.Media, error) {
|
func FindG711Format(desc *description.Session) (*format.G711, *description.Media, error) {
|
||||||
var g711Format *format.G711
|
var g711Format *format.G711
|
||||||
g711Media := desc.FindFormat(&g711Format)
|
g711Media := desc.FindFormat(&g711Format)
|
||||||
if g711Media == nil {
|
if g711Media == nil {
|
||||||
@ -22,20 +21,20 @@ func CheckG711Format(desc *description.Session) (*format.G711, *description.Medi
|
|||||||
return g711Format, g711Media, nil
|
return g711Format, g711Media, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProcG711 processes G711 flow and returns PTS and AU.
|
// ProcessG711 processes G711 flow and returns PTS and AU.
|
||||||
func ProcG711(c *gortsplib.Client, g711Media *description.Media, g711RTPDec *rtplpcm.Decoder, pkt *rtp.Packet) (
|
func ProcessG711(c *gortsplib.Client, g711Media *description.Media, g711RTPDec *rtplpcm.Decoder, pkt *rtp.Packet, t string,
|
||||||
|
) (
|
||||||
int64, []byte, error) {
|
int64, []byte, error) {
|
||||||
// Decode timestamp.
|
// Decode timestamp.
|
||||||
pts, ok := c.PacketPTS2(g711Media, pkt)
|
pts, ok := c.PacketPTS2(g711Media, pkt)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("waiting for timestamp\n")
|
return 0, nil, fmt.Errorf("[%v]: waiting for timestamp\n", t)
|
||||||
return 0, nil, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract access unit from RTP packets.
|
// Extract access unit from RTP packets.
|
||||||
au, err := g711RTPDec.Decode(pkt)
|
au, err := g711RTPDec.Decode(pkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, fmt.Errorf("decoding G711 RTP packet: %w", err)
|
return 0, nil, fmt.Errorf("[%v]: decoding RTP packet error: %w", t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return pts, au, nil
|
return pts, au, nil
|
@ -1,4 +1,4 @@
|
|||||||
package rtsp
|
package formats
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/gen2brain/aac-go"
|
"github.com/gen2brain/aac-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConvertG711ToAAC converts PCM to AAC.
|
// ConvertG711ToAAC converts G711 to AAC.
|
||||||
func ConvertG711ToAAC(g711Samples []byte, mulaw bool) ([]byte, error) {
|
func ConvertG711ToAAC(g711Samples []byte, mulaw bool) ([]byte, error) {
|
||||||
var pcmSamples []byte
|
var pcmSamples []byte
|
||||||
if mulaw {
|
if mulaw {
|
@ -1,13 +1,14 @@
|
|||||||
package converter
|
package formats
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/format"
|
"github.com/bluenviron/gortsplib/v4/pkg/format"
|
||||||
"github.com/bluenviron/mediacommon/v2/pkg/codecs/h264"
|
"github.com/bluenviron/mediacommon/pkg/codecs/h264"
|
||||||
"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts"
|
"github.com/bluenviron/mediacommon/pkg/formats/mpegts"
|
||||||
)
|
)
|
||||||
|
|
||||||
func multiplyAndDivide(v, m, d int64) int64 {
|
func multiplyAndDivide(v, m, d int64) int64 {
|
||||||
@ -22,19 +23,18 @@ type MpegtsMuxer struct {
|
|||||||
H264Format *format.H264
|
H264Format *format.H264
|
||||||
Mpeg4AudioFormat *format.MPEG4Audio
|
Mpeg4AudioFormat *format.MPEG4Audio
|
||||||
|
|
||||||
f *os.File
|
f *os.File
|
||||||
b *bufio.Writer
|
b *bufio.Writer
|
||||||
w *mpegts.Writer
|
w *mpegts.Writer
|
||||||
h264Track *mpegts.Track
|
h264Track *mpegts.Track
|
||||||
// mpeg4AudioTrack *mpegts.Track
|
mpeg4AudioTrack *mpegts.Track
|
||||||
dtsExtractor *h264.DTSExtractor
|
dtsExtractor *h264.DTSExtractor2
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize initializes a MpegtsMuxer.
|
// Initialize initializes a MpegtsMuxer.
|
||||||
func (e *MpegtsMuxer) Initialize() error {
|
func (e *MpegtsMuxer) Initialize() error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
e.f, err = os.Create(e.FileName)
|
e.f, err = os.Create(e.FileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -45,27 +45,21 @@ func (e *MpegtsMuxer) Initialize() error {
|
|||||||
Codec: &mpegts.CodecH264{},
|
Codec: &mpegts.CodecH264{},
|
||||||
}
|
}
|
||||||
|
|
||||||
//e.mpeg4AudioTrack = &mpegts.Track{
|
e.mpeg4AudioTrack = &mpegts.Track{
|
||||||
// Codec: &mpegts.CodecMPEG4Audio{
|
Codec: &mpegts.CodecMPEG4Audio{
|
||||||
// // Config: *e.mpeg4AudioFormat.Config,
|
Config: mpeg4audio.Config{},
|
||||||
// },
|
},
|
||||||
//}
|
}
|
||||||
|
|
||||||
e.w = mpegts.NewWriter(e.b, []*mpegts.Track{e.h264Track}) // add e.mpeg4AudioTrack
|
e.w = mpegts.NewWriter(e.b, []*mpegts.Track{e.h264Track, e.mpeg4AudioTrack})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close closes all the MpegtsMuxer resources.
|
// Close closes all the MpegtsMuxer resources.
|
||||||
func (e *MpegtsMuxer) Close() {
|
func (e *MpegtsMuxer) Close() {
|
||||||
err := e.b.Flush()
|
e.b.Flush()
|
||||||
if err != nil {
|
e.f.Close()
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
err = e.f.Close()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteH264 writes a H264 access unit into MPEG-TS.
|
// WriteH264 writes a H264 access unit into MPEG-TS.
|
||||||
@ -79,29 +73,27 @@ func (e *MpegtsMuxer) WriteH264(au [][]byte, pts int64) error {
|
|||||||
idrPresent := false
|
idrPresent := false
|
||||||
|
|
||||||
for _, nalu := range au {
|
for _, nalu := range au {
|
||||||
if len(nalu) != 0 {
|
typ := h264.NALUType(nalu[0] & 0x1F)
|
||||||
typ := h264.NALUType(nalu[0] & 0x1F)
|
switch typ {
|
||||||
switch typ {
|
case h264.NALUTypeSPS:
|
||||||
case h264.NALUTypeSPS:
|
e.H264Format.SPS = nalu
|
||||||
e.H264Format.SPS = nalu
|
continue
|
||||||
continue
|
|
||||||
|
|
||||||
case h264.NALUTypePPS:
|
case h264.NALUTypePPS:
|
||||||
e.H264Format.PPS = nalu
|
e.H264Format.PPS = nalu
|
||||||
continue
|
continue
|
||||||
|
|
||||||
case h264.NALUTypeAccessUnitDelimiter:
|
case h264.NALUTypeAccessUnitDelimiter:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
case h264.NALUTypeIDR:
|
case h264.NALUTypeIDR:
|
||||||
idrPresent = true
|
idrPresent = true
|
||||||
|
|
||||||
case h264.NALUTypeNonIDR:
|
case h264.NALUTypeNonIDR:
|
||||||
nonIDRPresent = true
|
nonIDRPresent = true
|
||||||
}
|
|
||||||
|
|
||||||
filteredAU = append(filteredAU, nalu)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filteredAU = append(filteredAU, nalu)
|
||||||
}
|
}
|
||||||
|
|
||||||
au = filteredAU
|
au = filteredAU
|
||||||
@ -120,7 +112,7 @@ func (e *MpegtsMuxer) WriteH264(au [][]byte, pts int64) error {
|
|||||||
if !idrPresent {
|
if !idrPresent {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
e.dtsExtractor = h264.NewDTSExtractor()
|
e.dtsExtractor = h264.NewDTSExtractor2()
|
||||||
}
|
}
|
||||||
|
|
||||||
dts, err := e.dtsExtractor.Extract(au, pts)
|
dts, err := e.dtsExtractor.Extract(au, pts)
|
||||||
@ -129,14 +121,13 @@ func (e *MpegtsMuxer) WriteH264(au [][]byte, pts int64) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Encode into MPEG-TS.
|
// Encode into MPEG-TS.
|
||||||
return e.w.WriteH264(e.h264Track, pts, dts, au)
|
return e.w.WriteH2642(e.h264Track, pts, dts, au)
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeMPEG4Audio writes MPEG-4 audio access units into MPEG-TS.
|
// WriteAAC writes MPEG-4 audio access units into MPEG-TS.
|
||||||
//func (e *MpegtsMuxer) writeMPEG4Audio(aus [][]byte, pts int64) error {
|
func (e *MpegtsMuxer) WriteAAC(aus [][]byte, pts int64) error {
|
||||||
// e.mutex.Lock()
|
e.mutex.Lock()
|
||||||
// defer e.mutex.Unlock()
|
defer e.mutex.Unlock()
|
||||||
//
|
|
||||||
// return e.w.WriteMPEG4Audio(
|
return e.w.WriteMPEG4Audio(e.mpeg4AudioTrack, multiplyAndDivide(pts, 90000, int64(8000)), aus)
|
||||||
// e.mpeg4AudioTrack, multiplyAndDivide(pts, 90000, int64(e.Mpeg4AudioFormat.ClockRate())), aus)
|
}
|
||||||
//}
|
|
96
writer/internal/ingest/formats/h264.go
Normal file
96
writer/internal/ingest/formats/h264.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package formats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/bluenviron/gortsplib/v4"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/format"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/format/rtph264"
|
||||||
|
"github.com/bluenviron/mediacommon/v2/pkg/codecs/h264"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
"image"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FindH264Format finds the H264 media and format.
|
||||||
|
func FindH264Format(desc *description.Session) (*format.H264, *description.Media, error) {
|
||||||
|
var h264Format *format.H264
|
||||||
|
h264Media := desc.FindFormat(&h264Format)
|
||||||
|
if h264Media == nil {
|
||||||
|
return nil, nil, errors.New("media H264 not found")
|
||||||
|
}
|
||||||
|
return h264Format, h264Media, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessH264 processes H264 flow and returns PTS and AU.
|
||||||
|
func ProcessH264(
|
||||||
|
c *gortsplib.Client,
|
||||||
|
h264Media *description.Media,
|
||||||
|
h264RTPDec *rtph264.Decoder,
|
||||||
|
pkt *rtp.Packet,
|
||||||
|
t string,
|
||||||
|
) (
|
||||||
|
int64,
|
||||||
|
[][]byte,
|
||||||
|
error) {
|
||||||
|
// Decode timestamp.
|
||||||
|
pts, ok := c.PacketPTS2(h264Media, pkt)
|
||||||
|
if !ok {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: waiting for timestamp\n", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract access unit from RTP packets.
|
||||||
|
au, err := h264RTPDec.Decode(pkt)
|
||||||
|
if err != nil {
|
||||||
|
if err != rtph264.ErrNonStartingPacketAndNoPrevious && err != rtph264.ErrMorePacketsNeeded {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: decoding RTP packet error: %w", t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pts, au, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessH264RGBA processes H264 flow and returns PTS and IMG.
|
||||||
|
func ProcessH264RGBA(
|
||||||
|
c *gortsplib.Client,
|
||||||
|
h264Media *description.Media,
|
||||||
|
h264RTPDec *rtph264.Decoder,
|
||||||
|
h264Dec *H264Decoder,
|
||||||
|
pkt *rtp.Packet,
|
||||||
|
firstRandomAccess bool,
|
||||||
|
t string,
|
||||||
|
) (
|
||||||
|
int64,
|
||||||
|
*image.RGBA,
|
||||||
|
error) {
|
||||||
|
// Decode timestamp.
|
||||||
|
pts, ok := c.PacketPTS2(h264Media, pkt)
|
||||||
|
if !ok {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: waiting for timestamp\n", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract access units from RTP packets.
|
||||||
|
au, err := h264RTPDec.Decode(pkt)
|
||||||
|
if err != rtph264.ErrNonStartingPacketAndNoPrevious && err != rtph264.ErrMorePacketsNeeded {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: decoding RTP packet error: %w", t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for a random access unit.
|
||||||
|
if !firstRandomAccess && !h264.IsRandomAccess(au) {
|
||||||
|
return 0, nil, nil
|
||||||
|
}
|
||||||
|
firstRandomAccess = true
|
||||||
|
|
||||||
|
// Convert H264 access units into RGBA frames.
|
||||||
|
img, err := h264Dec.Decode(au)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: convert into RGBA frames error\n", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for a frame.
|
||||||
|
if img == nil {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: frame not found\n", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pts, img, nil
|
||||||
|
}
|
161
writer/internal/ingest/formats/h264_decoder.go
Normal file
161
writer/internal/ingest/formats/h264_decoder.go
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
package formats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"runtime"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/bluenviron/mediacommon/v2/pkg/codecs/h264"
|
||||||
|
)
|
||||||
|
|
||||||
|
// #cgo pkg-config: libavcodec libavutil libswscale
|
||||||
|
// #include <libavcodec/avcodec.h>
|
||||||
|
// #include <libavutil/imgutils.h>
|
||||||
|
// #include <libswscale/swscale.h>
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
func frameDataH264(frame *C.AVFrame) **C.uint8_t {
|
||||||
|
return (**C.uint8_t)(unsafe.Pointer(&frame.data[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func frameLineSizeH264(frame *C.AVFrame) *C.int {
|
||||||
|
return (*C.int)(unsafe.Pointer(&frame.linesize[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// H264Decoder is a wrapper around FFmpeg's H264 decoder.
|
||||||
|
type H264Decoder struct {
|
||||||
|
codecCtx *C.AVCodecContext
|
||||||
|
yuv420Frame *C.AVFrame
|
||||||
|
rgbaFrame *C.AVFrame
|
||||||
|
rgbaFramePtr []uint8
|
||||||
|
swsCtx *C.struct_SwsContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize initializes a H264Decoder.
|
||||||
|
func (d *H264Decoder) Initialize() error {
|
||||||
|
codec := C.avcodec_find_decoder(C.AV_CODEC_ID_H264)
|
||||||
|
if codec == nil {
|
||||||
|
return fmt.Errorf("avcodec_find_decoder() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
d.codecCtx = C.avcodec_alloc_context3(codec)
|
||||||
|
if d.codecCtx == nil {
|
||||||
|
return fmt.Errorf("avcodec_alloc_context3() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
res := C.avcodec_open2(d.codecCtx, codec, nil)
|
||||||
|
if res < 0 {
|
||||||
|
C.avcodec_close(d.codecCtx)
|
||||||
|
return fmt.Errorf("avcodec_open2() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
d.yuv420Frame = C.av_frame_alloc()
|
||||||
|
if d.yuv420Frame == nil {
|
||||||
|
C.avcodec_close(d.codecCtx)
|
||||||
|
return fmt.Errorf("av_frame_alloc() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the decoder.
|
||||||
|
func (d *H264Decoder) Close() {
|
||||||
|
if d.swsCtx != nil {
|
||||||
|
C.sws_freeContext(d.swsCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.rgbaFrame != nil {
|
||||||
|
C.av_frame_free(&d.rgbaFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
C.av_frame_free(&d.yuv420Frame)
|
||||||
|
C.avcodec_close(d.codecCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *H264Decoder) reinitDynamicStuff() error {
|
||||||
|
if d.swsCtx != nil {
|
||||||
|
C.sws_freeContext(d.swsCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.rgbaFrame != nil {
|
||||||
|
C.av_frame_free(&d.rgbaFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.rgbaFrame = C.av_frame_alloc()
|
||||||
|
if d.rgbaFrame == nil {
|
||||||
|
return fmt.Errorf("av_frame_alloc() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
d.rgbaFrame.format = C.AV_PIX_FMT_RGBA
|
||||||
|
d.rgbaFrame.width = d.yuv420Frame.width
|
||||||
|
d.rgbaFrame.height = d.yuv420Frame.height
|
||||||
|
d.rgbaFrame.color_range = C.AVCOL_RANGE_JPEG
|
||||||
|
|
||||||
|
res := C.av_frame_get_buffer(d.rgbaFrame, 1)
|
||||||
|
if res < 0 {
|
||||||
|
return fmt.Errorf("av_frame_get_buffer() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
d.swsCtx = C.sws_getContext(d.yuv420Frame.width, d.yuv420Frame.height, int32(d.yuv420Frame.format),
|
||||||
|
d.rgbaFrame.width, d.rgbaFrame.height, (int32)(d.rgbaFrame.format), C.SWS_BILINEAR, nil, nil, nil)
|
||||||
|
if d.swsCtx == nil {
|
||||||
|
return fmt.Errorf("sws_getContext() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
rgbaFrameSize := C.av_image_get_buffer_size((int32)(d.rgbaFrame.format), d.rgbaFrame.width, d.rgbaFrame.height, 1)
|
||||||
|
d.rgbaFramePtr = (*[1 << 30]uint8)(unsafe.Pointer(d.rgbaFrame.data[0]))[:rgbaFrameSize:rgbaFrameSize]
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode decodes a RGBA image from H264.
|
||||||
|
func (d *H264Decoder) Decode(au [][]byte) (*image.RGBA, error) {
|
||||||
|
// Encode access unit into Annex-B.
|
||||||
|
annexb, err := h264.AnnexB(au).Marshal()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send access unit to decoder.
|
||||||
|
var pkt C.AVPacket
|
||||||
|
ptr := &annexb[0]
|
||||||
|
var p runtime.Pinner
|
||||||
|
p.Pin(ptr)
|
||||||
|
pkt.data = (*C.uint8_t)(ptr)
|
||||||
|
pkt.size = (C.int)(len(annexb))
|
||||||
|
res := C.avcodec_send_packet(d.codecCtx, &pkt)
|
||||||
|
p.Unpin()
|
||||||
|
if res < 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receive frame if available.
|
||||||
|
res = C.avcodec_receive_frame(d.codecCtx, d.yuv420Frame)
|
||||||
|
if res < 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If frame size has changed, allocate needed objects.
|
||||||
|
if d.rgbaFrame == nil || d.rgbaFrame.width != d.yuv420Frame.width || d.rgbaFrame.height != d.yuv420Frame.height {
|
||||||
|
err := d.reinitDynamicStuff()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert color space from YUV420 to RGBA.
|
||||||
|
res = C.sws_scale(d.swsCtx, frameDataH264(d.yuv420Frame), frameLineSizeH264(d.yuv420Frame),
|
||||||
|
0, d.yuv420Frame.height, frameDataH264(d.rgbaFrame), frameLineSizeH264(d.rgbaFrame))
|
||||||
|
if res < 0 {
|
||||||
|
return nil, fmt.Errorf("sws_scale() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Embed frame into an image.RGBA.
|
||||||
|
return &image.RGBA{
|
||||||
|
Pix: d.rgbaFramePtr,
|
||||||
|
Stride: 4 * (int)(d.rgbaFrame.width),
|
||||||
|
Rect: image.Rectangle{
|
||||||
|
Max: image.Point{(int)(d.rgbaFrame.width), (int)(d.rgbaFrame.height)},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
69
writer/internal/ingest/formats/h265.go
Normal file
69
writer/internal/ingest/formats/h265.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package formats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/bluenviron/gortsplib/v4"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/format"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/format/rtph265"
|
||||||
|
"github.com/bluenviron/mediacommon/v2/pkg/codecs/h265"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
"image"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FindH265Format finds the H265 media and format.
|
||||||
|
func FindH265Format(desc *description.Session) (*format.H265, *description.Media, error) {
|
||||||
|
var h265Format *format.H265
|
||||||
|
h265Media := desc.FindFormat(&h265Format)
|
||||||
|
if h265Media == nil {
|
||||||
|
return nil, nil, errors.New("media H265 not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return h265Format, h265Media, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessH265RGBA processes H265 flow and returns PTS and IMG.
|
||||||
|
func ProcessH265RGBA(
|
||||||
|
c *gortsplib.Client,
|
||||||
|
h265Media *description.Media,
|
||||||
|
h265RTPDec *rtph265.Decoder,
|
||||||
|
h265Dec *H265Decoder,
|
||||||
|
pkt *rtp.Packet,
|
||||||
|
firstRandomAccess bool,
|
||||||
|
t string,
|
||||||
|
) (
|
||||||
|
int64,
|
||||||
|
*image.RGBA,
|
||||||
|
error) {
|
||||||
|
// Decode timestamp.
|
||||||
|
pts, ok := c.PacketPTS2(h265Media, pkt)
|
||||||
|
if !ok {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: waiting for timestamp\n", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract access units from RTP packets.
|
||||||
|
au, err := h265RTPDec.Decode(pkt)
|
||||||
|
if err != rtph265.ErrNonStartingPacketAndNoPrevious && err != rtph265.ErrMorePacketsNeeded {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: decoding RTP packet error: %w", t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for a random access unit.
|
||||||
|
if !firstRandomAccess && !h265.IsRandomAccess(au) {
|
||||||
|
return 0, nil, nil
|
||||||
|
}
|
||||||
|
firstRandomAccess = true
|
||||||
|
|
||||||
|
// Convert H265 access units into RGBA frames.
|
||||||
|
img, err := h265Dec.Decode(au)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: convert into RGBA frames error\n", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for a frame.
|
||||||
|
if img == nil {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: frame not found\n", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pts, img, nil
|
||||||
|
}
|
161
writer/internal/ingest/formats/h265_decoder.go
Normal file
161
writer/internal/ingest/formats/h265_decoder.go
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
package formats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"runtime"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/bluenviron/mediacommon/v2/pkg/codecs/h264"
|
||||||
|
)
|
||||||
|
|
||||||
|
// #cgo pkg-config: libavcodec libavutil libswscale
|
||||||
|
// #include <libavcodec/avcodec.h>
|
||||||
|
// #include <libavutil/imgutils.h>
|
||||||
|
// #include <libswscale/swscale.h>
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
func frameDataH265(frame *C.AVFrame) **C.uint8_t {
|
||||||
|
return (**C.uint8_t)(unsafe.Pointer(&frame.data[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func frameLineSizeH265(frame *C.AVFrame) *C.int {
|
||||||
|
return (*C.int)(unsafe.Pointer(&frame.linesize[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// H265Decoder is a wrapper around FFmpeg's H265 decoder.
|
||||||
|
type H265Decoder struct {
|
||||||
|
codecCtx *C.AVCodecContext
|
||||||
|
yuv420Frame *C.AVFrame
|
||||||
|
rgbaFrame *C.AVFrame
|
||||||
|
rgbaFramePtr []uint8
|
||||||
|
swsCtx *C.struct_SwsContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize initializes a H265Decoder.
|
||||||
|
func (d *H265Decoder) Initialize() error {
|
||||||
|
codec := C.avcodec_find_decoder(C.AV_CODEC_ID_H265)
|
||||||
|
if codec == nil {
|
||||||
|
return fmt.Errorf("avcodec_find_decoder() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
d.codecCtx = C.avcodec_alloc_context3(codec)
|
||||||
|
if d.codecCtx == nil {
|
||||||
|
return fmt.Errorf("avcodec_alloc_context3() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
res := C.avcodec_open2(d.codecCtx, codec, nil)
|
||||||
|
if res < 0 {
|
||||||
|
C.avcodec_close(d.codecCtx)
|
||||||
|
return fmt.Errorf("avcodec_open2() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
d.yuv420Frame = C.av_frame_alloc()
|
||||||
|
if d.yuv420Frame == nil {
|
||||||
|
C.avcodec_close(d.codecCtx)
|
||||||
|
return fmt.Errorf("av_frame_alloc() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the decoder.
|
||||||
|
func (d *H265Decoder) Close() {
|
||||||
|
if d.swsCtx != nil {
|
||||||
|
C.sws_freeContext(d.swsCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.rgbaFrame != nil {
|
||||||
|
C.av_frame_free(&d.rgbaFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
C.av_frame_free(&d.yuv420Frame)
|
||||||
|
C.avcodec_close(d.codecCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *H265Decoder) reinitDynamicStuff() error {
|
||||||
|
if d.swsCtx != nil {
|
||||||
|
C.sws_freeContext(d.swsCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.rgbaFrame != nil {
|
||||||
|
C.av_frame_free(&d.rgbaFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.rgbaFrame = C.av_frame_alloc()
|
||||||
|
if d.rgbaFrame == nil {
|
||||||
|
return fmt.Errorf("av_frame_alloc() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
d.rgbaFrame.format = C.AV_PIX_FMT_RGBA
|
||||||
|
d.rgbaFrame.width = d.yuv420Frame.width
|
||||||
|
d.rgbaFrame.height = d.yuv420Frame.height
|
||||||
|
d.rgbaFrame.color_range = C.AVCOL_RANGE_JPEG
|
||||||
|
|
||||||
|
res := C.av_frame_get_buffer(d.rgbaFrame, 1)
|
||||||
|
if res < 0 {
|
||||||
|
return fmt.Errorf("av_frame_get_buffer() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
d.swsCtx = C.sws_getContext(d.yuv420Frame.width, d.yuv420Frame.height, int32(d.yuv420Frame.format),
|
||||||
|
d.rgbaFrame.width, d.rgbaFrame.height, (int32)(d.rgbaFrame.format), C.SWS_BILINEAR, nil, nil, nil)
|
||||||
|
if d.swsCtx == nil {
|
||||||
|
return fmt.Errorf("sws_getContext() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
rgbaFrameSize := C.av_image_get_buffer_size((int32)(d.rgbaFrame.format), d.rgbaFrame.width, d.rgbaFrame.height, 1)
|
||||||
|
d.rgbaFramePtr = (*[1 << 30]uint8)(unsafe.Pointer(d.rgbaFrame.data[0]))[:rgbaFrameSize:rgbaFrameSize]
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode decodes a RGBA image from H265.
|
||||||
|
func (d *H265Decoder) Decode(au [][]byte) (*image.RGBA, error) {
|
||||||
|
// encode access unit into Annex-B
|
||||||
|
annexb, err := h264.AnnexB(au).Marshal()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// send access unit to decoder
|
||||||
|
var pkt C.AVPacket
|
||||||
|
ptr := &annexb[0]
|
||||||
|
var p runtime.Pinner
|
||||||
|
p.Pin(ptr)
|
||||||
|
pkt.data = (*C.uint8_t)(ptr)
|
||||||
|
pkt.size = (C.int)(len(annexb))
|
||||||
|
res := C.avcodec_send_packet(d.codecCtx, &pkt)
|
||||||
|
p.Unpin()
|
||||||
|
if res < 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// receive frame if available
|
||||||
|
res = C.avcodec_receive_frame(d.codecCtx, d.yuv420Frame)
|
||||||
|
if res < 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// if frame size has changed, reallocate needed objects
|
||||||
|
if d.rgbaFrame == nil || d.rgbaFrame.width != d.yuv420Frame.width || d.rgbaFrame.height != d.yuv420Frame.height {
|
||||||
|
err := d.reinitDynamicStuff()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert color space from YUV420 to RGBA
|
||||||
|
res = C.sws_scale(d.swsCtx, frameDataH265(d.yuv420Frame), frameLineSizeH265(d.yuv420Frame),
|
||||||
|
0, d.yuv420Frame.height, frameDataH265(d.rgbaFrame), frameLineSizeH265(d.rgbaFrame))
|
||||||
|
if res < 0 {
|
||||||
|
return nil, fmt.Errorf("sws_scale() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// embed frame into an image.RGBA
|
||||||
|
return &image.RGBA{
|
||||||
|
Pix: d.rgbaFramePtr,
|
||||||
|
Stride: 4 * (int)(d.rgbaFrame.width),
|
||||||
|
Rect: image.Rectangle{
|
||||||
|
Max: image.Point{(int)(d.rgbaFrame.width), (int)(d.rgbaFrame.height)},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
48
writer/internal/ingest/formats/lpcm.go
Normal file
48
writer/internal/ingest/formats/lpcm.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package formats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/bluenviron/gortsplib/v4"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/format"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/format/rtplpcm"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FindLPCMFormat finds the LPCM media and format.
|
||||||
|
func FindLPCMFormat(desc *description.Session) (*format.LPCM, *description.Media, error) {
|
||||||
|
var lpcmFormat *format.LPCM
|
||||||
|
lpcmMedia := desc.FindFormat(&lpcmFormat)
|
||||||
|
if lpcmMedia == nil {
|
||||||
|
return nil, nil, errors.New("media LPCM not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return lpcmFormat, lpcmMedia, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessLPCM processes LPCM flow and returns PTS and SAMPLES.
|
||||||
|
func ProcessLPCM(
|
||||||
|
c *gortsplib.Client,
|
||||||
|
lpcmMedia *description.Media,
|
||||||
|
lpcmRTPDec *rtplpcm.Decoder,
|
||||||
|
pkt *rtp.Packet,
|
||||||
|
t string,
|
||||||
|
) (
|
||||||
|
int64,
|
||||||
|
[]byte,
|
||||||
|
error) {
|
||||||
|
// Decode timestamp.
|
||||||
|
pts, ok := c.PacketPTS2(lpcmMedia, pkt)
|
||||||
|
if !ok {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: waiting for timestamp\n", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract LPCM samples from RTP packets.
|
||||||
|
samples, err := lpcmRTPDec.Decode(pkt)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: decoding RTP packet error: %w", t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pts, samples, err
|
||||||
|
}
|
57
writer/internal/ingest/formats/mjpeg.go
Normal file
57
writer/internal/ingest/formats/mjpeg.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package formats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/bluenviron/gortsplib/v4"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/format"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpmjpeg"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
"image"
|
||||||
|
"image/jpeg"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FindMJPEGFormat finds the MJPEG media and format.
|
||||||
|
func FindMJPEGFormat(desc *description.Session) (*format.MJPEG, *description.Media, error) {
|
||||||
|
var mjpegFormat *format.MJPEG
|
||||||
|
mjpegMedia := desc.FindFormat(&mjpegFormat)
|
||||||
|
if mjpegMedia == nil {
|
||||||
|
return nil, nil, errors.New("media MJPEG not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return mjpegFormat, mjpegMedia, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessMJPEGRGBA processes MJPEG flow and returns PTS and IMG.
|
||||||
|
func ProcessMJPEGRGBA(
|
||||||
|
c *gortsplib.Client,
|
||||||
|
mjpegMedia *description.Media,
|
||||||
|
mjpegRTPDec *rtpmjpeg.Decoder,
|
||||||
|
pkt *rtp.Packet,
|
||||||
|
t string,
|
||||||
|
) (
|
||||||
|
int64,
|
||||||
|
image.Image,
|
||||||
|
error) {
|
||||||
|
// Decode timestamp.
|
||||||
|
pts, ok := c.PacketPTS2(mjpegMedia, pkt)
|
||||||
|
if !ok {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: waiting for timestamp\n", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract JPEG images from RTP packets.
|
||||||
|
enc, err := mjpegRTPDec.Decode(pkt)
|
||||||
|
if err != rtpmjpeg.ErrNonStartingPacketAndNoPrevious && err != rtpmjpeg.ErrMorePacketsNeeded {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: decoding RTP packet error: %w", t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert JPEG images into RGBA frames.
|
||||||
|
img, err := jpeg.Decode(bytes.NewReader(enc))
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: convert into RGBA frames error\n", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pts, img, nil
|
||||||
|
}
|
48
writer/internal/ingest/formats/opus.go
Normal file
48
writer/internal/ingest/formats/opus.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package formats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/bluenviron/gortsplib/v4"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/format"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpsimpleaudio"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FindOPUSFormat finds the OPUS media and format.
|
||||||
|
func FindOPUSFormat(desc *description.Session) (*format.Opus, *description.Media, error) {
|
||||||
|
var opusFormat *format.Opus
|
||||||
|
opusMedia := desc.FindFormat(&opusFormat)
|
||||||
|
if opusMedia == nil {
|
||||||
|
return nil, nil, errors.New("media OPUS not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return opusFormat, opusMedia, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessOPUS processes OPUS flow and returns PTS and OP.
|
||||||
|
func ProcessOPUS(
|
||||||
|
c *gortsplib.Client,
|
||||||
|
opusMedia *description.Media,
|
||||||
|
opusRTPDec *rtpsimpleaudio.Decoder,
|
||||||
|
pkt *rtp.Packet,
|
||||||
|
t string,
|
||||||
|
) (
|
||||||
|
int64,
|
||||||
|
[]byte,
|
||||||
|
error) {
|
||||||
|
// Decode timestamp.
|
||||||
|
pts, ok := c.PacketPTS2(opusMedia, pkt)
|
||||||
|
if !ok {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: waiting for timestamp\n", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract Opus packets from RTP packets.
|
||||||
|
op, err := opusRTPDec.Decode(pkt)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: decoding RTP packet error: %w", t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pts, op, nil
|
||||||
|
}
|
153
writer/internal/ingest/formats/vp8-vp9_decoder.go
Normal file
153
writer/internal/ingest/formats/vp8-vp9_decoder.go
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
package formats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"runtime"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// #cgo pkg-config: libavcodec libavutil libswscale
|
||||||
|
// #include <libavcodec/avcodec.h>
|
||||||
|
// #include <libavutil/imgutils.h>
|
||||||
|
// #include <libswscale/swscale.h>
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
func frameDataVP(frame *C.AVFrame) **C.uint8_t {
|
||||||
|
return (**C.uint8_t)(unsafe.Pointer(&frame.data[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func frameLineSizeVP(frame *C.AVFrame) *C.int {
|
||||||
|
return (*C.int)(unsafe.Pointer(&frame.linesize[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// VP8Decoder is a wrapper around FFmpeg's VP8 decoder.
|
||||||
|
type VPDecoder struct {
|
||||||
|
codecCtx *C.AVCodecContext
|
||||||
|
yuv420Frame *C.AVFrame
|
||||||
|
rgbaFrame *C.AVFrame
|
||||||
|
rgbaFramePtr []uint8
|
||||||
|
swsCtx *C.struct_SwsContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize initializes a VP8Decoder.
|
||||||
|
func (d *VPDecoder) Initialize() error {
|
||||||
|
codec := C.avcodec_find_decoder(C.AV_CODEC_ID_VP8)
|
||||||
|
if codec == nil {
|
||||||
|
return fmt.Errorf("avcodec_find_decoder() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
d.codecCtx = C.avcodec_alloc_context3(codec)
|
||||||
|
if d.codecCtx == nil {
|
||||||
|
return fmt.Errorf("avcodec_alloc_context3() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
res := C.avcodec_open2(d.codecCtx, codec, nil)
|
||||||
|
if res < 0 {
|
||||||
|
C.avcodec_close(d.codecCtx)
|
||||||
|
return fmt.Errorf("avcodec_open2() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
d.yuv420Frame = C.av_frame_alloc()
|
||||||
|
if d.yuv420Frame == nil {
|
||||||
|
C.avcodec_close(d.codecCtx)
|
||||||
|
return fmt.Errorf("av_frame_alloc() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the decoder.
|
||||||
|
func (d *VPDecoder) Close() {
|
||||||
|
if d.swsCtx != nil {
|
||||||
|
C.sws_freeContext(d.swsCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.rgbaFrame != nil {
|
||||||
|
C.av_frame_free(&d.rgbaFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
C.av_frame_free(&d.yuv420Frame)
|
||||||
|
C.avcodec_close(d.codecCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *VPDecoder) reinitDynamicStuff() error {
|
||||||
|
if d.swsCtx != nil {
|
||||||
|
C.sws_freeContext(d.swsCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.rgbaFrame != nil {
|
||||||
|
C.av_frame_free(&d.rgbaFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.rgbaFrame = C.av_frame_alloc()
|
||||||
|
if d.rgbaFrame == nil {
|
||||||
|
return fmt.Errorf("av_frame_alloc() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
d.rgbaFrame.format = C.AV_PIX_FMT_RGBA
|
||||||
|
d.rgbaFrame.width = d.yuv420Frame.width
|
||||||
|
d.rgbaFrame.height = d.yuv420Frame.height
|
||||||
|
d.rgbaFrame.color_range = C.AVCOL_RANGE_JPEG
|
||||||
|
|
||||||
|
res := C.av_frame_get_buffer(d.rgbaFrame, 1)
|
||||||
|
if res < 0 {
|
||||||
|
return fmt.Errorf("av_frame_get_buffer() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
d.swsCtx = C.sws_getContext(d.yuv420Frame.width, d.yuv420Frame.height, int32(d.yuv420Frame.format),
|
||||||
|
d.rgbaFrame.width, d.rgbaFrame.height, (int32)(d.rgbaFrame.format), C.SWS_BILINEAR, nil, nil, nil)
|
||||||
|
if d.swsCtx == nil {
|
||||||
|
return fmt.Errorf("sws_getContext() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
rgbaFrameSize := C.av_image_get_buffer_size((int32)(d.rgbaFrame.format), d.rgbaFrame.width, d.rgbaFrame.height, 1)
|
||||||
|
d.rgbaFramePtr = (*[1 << 30]uint8)(unsafe.Pointer(d.rgbaFrame.data[0]))[:rgbaFrameSize:rgbaFrameSize]
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode decodes a RGBA image from VP8.
|
||||||
|
func (d *VPDecoder) Decode(au []byte) (*image.RGBA, error) {
|
||||||
|
// send access unit to decoder
|
||||||
|
var pkt C.AVPacket
|
||||||
|
ptr := &au[0]
|
||||||
|
var p runtime.Pinner
|
||||||
|
p.Pin(ptr)
|
||||||
|
pkt.data = (*C.uint8_t)(ptr)
|
||||||
|
pkt.size = (C.int)(len(au))
|
||||||
|
res := C.avcodec_send_packet(d.codecCtx, &pkt)
|
||||||
|
p.Unpin()
|
||||||
|
if res < 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// receive frame if available
|
||||||
|
res = C.avcodec_receive_frame(d.codecCtx, d.yuv420Frame)
|
||||||
|
if res < 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// if frame size has changed, allocate needed objects
|
||||||
|
if d.rgbaFrame == nil || d.rgbaFrame.width != d.yuv420Frame.width || d.rgbaFrame.height != d.yuv420Frame.height {
|
||||||
|
err := d.reinitDynamicStuff()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert color space from YUV420 to RGBA
|
||||||
|
res = C.sws_scale(d.swsCtx, frameDataVP(d.yuv420Frame), frameLineSizeVP(d.yuv420Frame),
|
||||||
|
0, d.yuv420Frame.height, frameDataVP(d.rgbaFrame), frameLineSizeVP(d.rgbaFrame))
|
||||||
|
if res < 0 {
|
||||||
|
return nil, fmt.Errorf("sws_scale() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// embed frame into an image.RGBA
|
||||||
|
return &image.RGBA{
|
||||||
|
Pix: d.rgbaFramePtr,
|
||||||
|
Stride: 4 * (int)(d.rgbaFrame.width),
|
||||||
|
Rect: image.Rectangle{
|
||||||
|
Max: image.Point{(int)(d.rgbaFrame.width), (int)(d.rgbaFrame.height)},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
61
writer/internal/ingest/formats/vp8.go
Normal file
61
writer/internal/ingest/formats/vp8.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package formats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/bluenviron/gortsplib/v4"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/format"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp8"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
"image"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FindVP8Format finds the VP8 media and format.
|
||||||
|
func FindVP8Format(desc *description.Session) (*format.VP8, *description.Media, error) {
|
||||||
|
var vp8Format *format.VP8
|
||||||
|
vp8Media := desc.FindFormat(&vp8Format)
|
||||||
|
if vp8Media == nil {
|
||||||
|
return nil, nil, errors.New("media VP8 not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return vp8Format, vp8Media, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessVP8RGBA processes VP8 flow and returns PTS and IMG.
|
||||||
|
func ProcessVP8RGBA(
|
||||||
|
c *gortsplib.Client,
|
||||||
|
vp8Media *description.Media,
|
||||||
|
vp8RTPDec *rtpvp8.Decoder,
|
||||||
|
vp8Dec *VPDecoder,
|
||||||
|
pkt *rtp.Packet,
|
||||||
|
t string,
|
||||||
|
) (
|
||||||
|
int64,
|
||||||
|
*image.RGBA,
|
||||||
|
error) {
|
||||||
|
// Decode timestamp.
|
||||||
|
pts, ok := c.PacketPTS2(vp8Media, pkt)
|
||||||
|
if !ok {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: waiting for timestamp\n", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract access units from RTP packets.
|
||||||
|
au, err := vp8RTPDec.Decode(pkt)
|
||||||
|
if err != rtpvp8.ErrNonStartingPacketAndNoPrevious && err != rtpvp8.ErrMorePacketsNeeded {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: decoding RTP packet error: %w", t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert VP8 access units into RGBA frames.
|
||||||
|
img, err := vp8Dec.Decode(au)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: convert into RGBA frames error\n", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for a frame.
|
||||||
|
if img == nil {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: frame not found\n", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pts, img, nil
|
||||||
|
}
|
62
writer/internal/ingest/formats/vp9.go
Normal file
62
writer/internal/ingest/formats/vp9.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package formats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/bluenviron/gortsplib/v4"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/format"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp8"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp9"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
"image"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FindVP9Format finds the VP9 media and format.
|
||||||
|
func FindVP9Format(desc *description.Session) (*format.VP9, *description.Media, error) {
|
||||||
|
var vp9Format *format.VP9
|
||||||
|
vp9Media := desc.FindFormat(&vp9Format)
|
||||||
|
if vp9Media == nil {
|
||||||
|
return nil, nil, errors.New("media VP9 not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return vp9Format, vp9Media, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessVP9RGBA processes VP9 flow and returns PTS and IMG.
|
||||||
|
func ProcessVP9RGBA(
|
||||||
|
c *gortsplib.Client,
|
||||||
|
vp9Media *description.Media,
|
||||||
|
vp9RTPDec *rtpvp9.Decoder,
|
||||||
|
vp9Dec *VPDecoder,
|
||||||
|
pkt *rtp.Packet,
|
||||||
|
t string,
|
||||||
|
) (
|
||||||
|
int64,
|
||||||
|
*image.RGBA,
|
||||||
|
error) {
|
||||||
|
// Decode timestamp.
|
||||||
|
pts, ok := c.PacketPTS2(vp9Media, pkt)
|
||||||
|
if !ok {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: waiting for timestamp\n", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract access units from RTP packets.
|
||||||
|
au, err := vp9RTPDec.Decode(pkt)
|
||||||
|
if err != rtpvp8.ErrNonStartingPacketAndNoPrevious && err != rtpvp8.ErrMorePacketsNeeded {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: decoding RTP packet error: %w", t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert VP8 access units into RGBA frames.
|
||||||
|
img, err := vp9Dec.Decode(au)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: convert into RGBA frames error\n", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for a frame.
|
||||||
|
if img == nil {
|
||||||
|
return 0, nil, fmt.Errorf("[%v]: frame not found\n", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pts, img, nil
|
||||||
|
}
|
@ -1,43 +0,0 @@
|
|||||||
package rtsp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/bluenviron/gortsplib/v4"
|
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/format"
|
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/format/rtph264"
|
|
||||||
"github.com/pion/rtp"
|
|
||||||
"log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CheckH264Format finds the H264 media and format.
|
|
||||||
func CheckH264Format(desc *description.Session) (*format.H264, *description.Media, error) {
|
|
||||||
var h264Format *format.H264
|
|
||||||
h264Media := desc.FindFormat(&h264Format)
|
|
||||||
if h264Media == nil {
|
|
||||||
return nil, nil, errors.New("media H264 not found")
|
|
||||||
}
|
|
||||||
return h264Format, h264Media, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProcH264 processes H264 flow and returns PTS and AU.
|
|
||||||
func ProcH264(c *gortsplib.Client, h264Media *description.Media, h264RTPDec *rtph264.Decoder, pkt *rtp.Packet) (
|
|
||||||
int64, [][]byte, error) {
|
|
||||||
// Decode timestamp.
|
|
||||||
pts, ok := c.PacketPTS2(h264Media, pkt)
|
|
||||||
if !ok {
|
|
||||||
log.Printf("waiting for timestamp\n")
|
|
||||||
return 0, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract access unit from RTP packets.
|
|
||||||
au, err := h264RTPDec.Decode(pkt)
|
|
||||||
if err != nil {
|
|
||||||
if err != rtph264.ErrNonStartingPacketAndNoPrevious && err != rtph264.ErrMorePacketsNeeded {
|
|
||||||
return 0, nil, fmt.Errorf("decoding H264 RTP packet: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pts, au, nil
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -38,7 +38,7 @@ func WaitPeriod(period int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func FindResolution(Body []byte) string {
|
func FindResolution(Body []byte) string {
|
||||||
split := strings.Split(string(Body), "\n")
|
split := strings.Split(string(Body), "\r\n")
|
||||||
for _, line := range split {
|
for _, line := range split {
|
||||||
if strings.Contains(line, "a=x-dimensions:") {
|
if strings.Contains(line, "a=x-dimensions:") {
|
||||||
s, _ := strings.CutPrefix(line, "a=x-dimensions:")
|
s, _ := strings.CutPrefix(line, "a=x-dimensions:")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user