Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9268acf1ca | |||
| 55fdf1a647 | |||
| 5fe07aeea0 | |||
| e8b22bca99 | |||
| 5926c1deb9 | |||
| dd98edc48e | |||
| fb1cc7dfc2 | |||
| 7626a09c1c | |||
| db85533e74 | |||
| 5939c8acba | |||
| e985ad23a2 | |||
| 7452eb5e05 | |||
| 5f9788209d | |||
| c07ddb8309 |
+1
-1
@@ -16,7 +16,7 @@ import (
|
|||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "1.3.2"
|
var Version = "1.4.0"
|
||||||
var UserAgent = "go2rtc/" + Version
|
var UserAgent = "go2rtc/" + Version
|
||||||
|
|
||||||
var ConfigPath string
|
var ConfigPath string
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ var defaults = map[string]string{
|
|||||||
"h265": "-c:v libx265 -g 50 -profile:v high -level:v 5.1 -preset:v superfast -tune:v zerolatency",
|
"h265": "-c:v libx265 -g 50 -profile:v high -level:v 5.1 -preset:v superfast -tune:v zerolatency",
|
||||||
"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
|
"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
|
||||||
|
|
||||||
"opus": "-c:a libopus -ar:a 48000 -ac:a 2",
|
// https://ffmpeg.org/ffmpeg-codecs.html#libopus-1
|
||||||
|
"opus": "-c:a libopus -ar:a 48000 -ac:a 2 -application:a voip -compression_level:a 0",
|
||||||
"pcmu": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
|
"pcmu": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
|
||||||
"pcmu/16000": "-c:a pcm_mulaw -ar:a 16000 -ac:a 1",
|
"pcmu/16000": "-c:a pcm_mulaw -ar:a 16000 -ac:a 1",
|
||||||
"pcmu/48000": "-c:a pcm_mulaw -ar:a 48000 -ac:a 1",
|
"pcmu/48000": "-c:a pcm_mulaw -ar:a 48000 -ac:a 1",
|
||||||
@@ -70,8 +71,7 @@ var defaults = map[string]string{
|
|||||||
"aac": "-c:a aac", // keep sample rate and channels
|
"aac": "-c:a aac", // keep sample rate and channels
|
||||||
"aac/16000": "-c:a aac -ar:a 16000 -ac:a 1",
|
"aac/16000": "-c:a aac -ar:a 16000 -ac:a 1",
|
||||||
"mp3": "-c:a libmp3lame -q:a 8",
|
"mp3": "-c:a libmp3lame -q:a 8",
|
||||||
"pcm": "-c:a pcm_s16be",
|
"pcm": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
|
||||||
"pcm/8000": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
|
|
||||||
"pcm/16000": "-c:a pcm_s16be -ar:a 16000 -ac:a 1",
|
"pcm/16000": "-c:a pcm_s16be -ar:a 16000 -ac:a 1",
|
||||||
"pcm/48000": "-c:a pcm_s16be -ar:a 48000 -ac:a 1",
|
"pcm/48000": "-c:a pcm_s16be -ar:a 48000 -ac:a 1",
|
||||||
|
|
||||||
|
|||||||
+18
-1
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -48,6 +49,9 @@ const keepalive = 5 * time.Second
|
|||||||
|
|
||||||
var sessions = map[string]*Session{}
|
var sessions = map[string]*Session{}
|
||||||
|
|
||||||
|
// once I saw 404 on MP4 segment, so better to use mutex
|
||||||
|
var sessionsMu sync.RWMutex
|
||||||
|
|
||||||
func handlerStream(w http.ResponseWriter, r *http.Request) {
|
func handlerStream(w http.ResponseWriter, r *http.Request) {
|
||||||
// CORS important for Chromecast
|
// CORS important for Chromecast
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
@@ -128,11 +132,16 @@ segment.ts?id=` + sid + `&n=%d
|
|||||||
segment.ts?id=` + sid + `&n=%d`
|
segment.ts?id=` + sid + `&n=%d`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sessionsMu.Lock()
|
||||||
sessions[sid] = session
|
sessions[sid] = session
|
||||||
|
sessionsMu.Unlock()
|
||||||
|
|
||||||
|
// Apple Safari can play FLAC codec, but fail it it in m3u8 playlist
|
||||||
|
codecs := strings.Replace(cons.MimeCodecs(), mp4.MimeFlac, mp4.MimeAAC, 1)
|
||||||
|
|
||||||
// bandwidth important for Safari, codecs useful for smooth playback
|
// bandwidth important for Safari, codecs useful for smooth playback
|
||||||
data := []byte(`#EXTM3U
|
data := []byte(`#EXTM3U
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS="` + cons.MimeCodecs() + `"
|
#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS="` + codecs + `"
|
||||||
hls/playlist.m3u8?id=` + sid)
|
hls/playlist.m3u8?id=` + sid)
|
||||||
|
|
||||||
if _, err := w.Write(data); err != nil {
|
if _, err := w.Write(data); err != nil {
|
||||||
@@ -150,7 +159,9 @@ func handlerPlaylist(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sid := r.URL.Query().Get("id")
|
sid := r.URL.Query().Get("id")
|
||||||
|
sessionsMu.RLock()
|
||||||
session := sessions[sid]
|
session := sessions[sid]
|
||||||
|
sessionsMu.RUnlock()
|
||||||
if session == nil {
|
if session == nil {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
@@ -173,7 +184,9 @@ func handlerSegmentTS(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sid := r.URL.Query().Get("id")
|
sid := r.URL.Query().Get("id")
|
||||||
|
sessionsMu.RLock()
|
||||||
session := sessions[sid]
|
session := sessions[sid]
|
||||||
|
sessionsMu.RUnlock()
|
||||||
if session == nil {
|
if session == nil {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
@@ -212,7 +225,9 @@ func handlerInit(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sid := r.URL.Query().Get("id")
|
sid := r.URL.Query().Get("id")
|
||||||
|
sessionsMu.RLock()
|
||||||
session := sessions[sid]
|
session := sessions[sid]
|
||||||
|
sessionsMu.RUnlock()
|
||||||
if session == nil {
|
if session == nil {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
@@ -233,7 +248,9 @@ func handlerSegmentMP4(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sid := r.URL.Query().Get("id")
|
sid := r.URL.Query().Get("id")
|
||||||
|
sessionsMu.RLock()
|
||||||
session := sessions[sid]
|
session := sessions[sid]
|
||||||
|
sessionsMu.RUnlock()
|
||||||
if session == nil {
|
if session == nil {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
|
|||||||
+14
-10
@@ -4,13 +4,11 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
@@ -61,6 +59,7 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,17 +105,15 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
|||||||
cons := &mp4.Consumer{
|
cons := &mp4.Consumer{
|
||||||
RemoteAddr: tcp.RemoteAddr(r),
|
RemoteAddr: tcp.RemoteAddr(r),
|
||||||
UserAgent: r.UserAgent(),
|
UserAgent: r.UserAgent(),
|
||||||
Medias: core.ParseQuery(r.URL.Query()),
|
Medias: mp4.ParseQuery(r.URL.Query()),
|
||||||
}
|
}
|
||||||
|
|
||||||
var mu sync.Mutex
|
|
||||||
cons.Listen(func(msg any) {
|
cons.Listen(func(msg any) {
|
||||||
|
if exit == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if data, ok := msg.([]byte); ok {
|
if data, ok := msg.([]byte); ok {
|
||||||
mu.Lock()
|
if _, err := w.Write(data); err != nil {
|
||||||
_, err := w.Write(data)
|
|
||||||
mu.Unlock()
|
|
||||||
|
|
||||||
if err != nil && exit != nil {
|
|
||||||
select {
|
select {
|
||||||
case exit <- err:
|
case exit <- err:
|
||||||
default:
|
default:
|
||||||
@@ -128,6 +125,7 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,11 +136,13 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
|||||||
data, err := cons.Init()
|
data, err := cons.Init()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = w.Write(data); err != nil {
|
if _, err = w.Write(data); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +153,10 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
|||||||
if i, _ := strconv.Atoi(s); i > 0 {
|
if i, _ := strconv.Atoi(s); i > 0 {
|
||||||
duration = time.AfterFunc(time.Second*time.Duration(i), func() {
|
duration = time.AfterFunc(time.Second*time.Duration(i), func() {
|
||||||
if exit != nil {
|
if exit != nil {
|
||||||
exit <- nil
|
select {
|
||||||
|
case exit <- nil:
|
||||||
|
default:
|
||||||
|
}
|
||||||
exit = nil
|
exit = nil
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -161,6 +164,7 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = <-exit
|
err = <-exit
|
||||||
|
exit = nil
|
||||||
|
|
||||||
log.Trace().Err(err).Caller().Send()
|
log.Trace().Err(err).Caller().Send()
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,12 @@ func parseMedias(codecs string, parseAudio bool) (medias []*core.Media) {
|
|||||||
case mp4.MimeAAC:
|
case mp4.MimeAAC:
|
||||||
codec := &core.Codec{Name: core.CodecAAC}
|
codec := &core.Codec{Name: core.CodecAAC}
|
||||||
audios = append(audios, codec)
|
audios = append(audios, codec)
|
||||||
|
case mp4.MimeFlac:
|
||||||
|
audios = append(audios,
|
||||||
|
&core.Codec{Name: core.CodecPCMA},
|
||||||
|
&core.Codec{Name: core.CodecPCMU},
|
||||||
|
&core.Codec{Name: core.CodecPCM},
|
||||||
|
)
|
||||||
case mp4.MimeOpus:
|
case mp4.MimeOpus:
|
||||||
codec := &core.Codec{Name: core.CodecOpus}
|
codec := &core.Codec{Name: core.CodecOpus}
|
||||||
audios = append(audios, codec)
|
audios = append(audios, codec)
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ require (
|
|||||||
github.com/pion/stun v0.4.0
|
github.com/pion/stun v0.4.0
|
||||||
github.com/pion/webrtc/v3 v3.1.58
|
github.com/pion/webrtc/v3 v3.1.58
|
||||||
github.com/rs/zerolog v1.29.0
|
github.com/rs/zerolog v1.29.0
|
||||||
|
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3
|
||||||
|
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
|
||||||
github.com/stretchr/testify v1.8.2
|
github.com/stretchr/testify v1.8.2
|
||||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
|
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
|||||||
@@ -106,6 +106,10 @@ github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
|||||||
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
|
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
|
||||||
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
||||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||||
|
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs=
|
||||||
|
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
|
||||||
|
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA=
|
||||||
|
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
|||||||
+8
-1
@@ -99,9 +99,16 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
|
|||||||
case "8":
|
case "8":
|
||||||
c.Name = CodecPCMA
|
c.Name = CodecPCMA
|
||||||
c.ClockRate = 8000
|
c.ClockRate = 8000
|
||||||
|
case "10":
|
||||||
|
c.Name = CodecPCM
|
||||||
|
c.ClockRate = 44100
|
||||||
|
c.Channels = 2
|
||||||
|
case "11":
|
||||||
|
c.Name = CodecPCM
|
||||||
|
c.ClockRate = 44100
|
||||||
case "14":
|
case "14":
|
||||||
c.Name = CodecMP3
|
c.Name = CodecMP3
|
||||||
c.ClockRate = 44100
|
c.ClockRate = 90000 // it's not real sample rate
|
||||||
case "26":
|
case "26":
|
||||||
c.Name = CodecJPEG
|
c.Name = CodecJPEG
|
||||||
c.ClockRate = 90000
|
c.ClockRate = 90000
|
||||||
|
|||||||
+2
-1
@@ -27,7 +27,8 @@ const (
|
|||||||
CodecMP3 = "MPA" // payload: 14, aka MPEG-1 Layer III
|
CodecMP3 = "MPA" // payload: 14, aka MPEG-1 Layer III
|
||||||
CodecPCM = "L16" // Linear PCM
|
CodecPCM = "L16" // Linear PCM
|
||||||
|
|
||||||
CodecELD = "ELD" // AAC-ELD
|
CodecELD = "ELD" // AAC-ELD
|
||||||
|
CodecFLAC = "FLAC"
|
||||||
|
|
||||||
CodecAll = "ALL"
|
CodecAll = "ALL"
|
||||||
CodecAny = "ANY"
|
CodecAny = "ANY"
|
||||||
|
|||||||
+5
-1
@@ -93,7 +93,7 @@ func GetKind(name string) string {
|
|||||||
switch name {
|
switch name {
|
||||||
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG:
|
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG:
|
||||||
return KindVideo
|
return KindVideo
|
||||||
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecELD:
|
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecPCM, CodecELD, CodecFLAC:
|
||||||
return KindAudio
|
return KindAudio
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
@@ -136,6 +136,10 @@ func MarshalSDP(name string, medias []*Media) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
md.WithCodec(codec.PayloadType, name, codec.ClockRate, codec.Channels, codec.FmtpLine)
|
md.WithCodec(codec.PayloadType, name, codec.ClockRate, codec.Channels, codec.FmtpLine)
|
||||||
|
|
||||||
|
if media.ID != "" {
|
||||||
|
md.WithValueAttribute("control", media.ID)
|
||||||
|
}
|
||||||
|
|
||||||
sd.MediaDescriptions = append(sd.MediaDescriptions, md)
|
sd.MediaDescriptions = append(sd.MediaDescriptions, md)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-6
@@ -32,6 +32,16 @@ const (
|
|||||||
Mdat = "mdat"
|
Mdat = "mdat"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sampleIsNonSync = 0x10000
|
||||||
|
sampleDependsOn1 = 0x1000000
|
||||||
|
sampleDependsOn2 = 0x2000000
|
||||||
|
|
||||||
|
SampleVideoIFrame = sampleDependsOn2
|
||||||
|
SampleVideoNonIFrame = sampleDependsOn1 | sampleIsNonSync
|
||||||
|
SampleAudio = sampleIsNonSync
|
||||||
|
)
|
||||||
|
|
||||||
func (m *Movie) WriteFileType() {
|
func (m *Movie) WriteFileType() {
|
||||||
m.StartAtom(Ftyp)
|
m.StartAtom(Ftyp)
|
||||||
m.WriteString("iso5")
|
m.WriteString("iso5")
|
||||||
@@ -250,7 +260,7 @@ func (m *Movie) WriteAudioTrack(id uint32, codec string, timescale uint32, chann
|
|||||||
m.EndAtom() // TRAK
|
m.EndAtom() // TRAK
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Movie) WriteMovieFragment(seq, tid, duration, size uint32, time uint64) {
|
func (m *Movie) WriteMovieFragment(seq, tid, duration, size, flags uint32, time uint64) {
|
||||||
m.StartAtom(Moof)
|
m.StartAtom(Moof)
|
||||||
|
|
||||||
m.StartAtom(MoofMfhd)
|
m.StartAtom(MoofMfhd)
|
||||||
@@ -276,10 +286,10 @@ func (m *Movie) WriteMovieFragment(seq, tid, duration, size uint32, time uint64)
|
|||||||
TfhdDefaultSampleFlags |
|
TfhdDefaultSampleFlags |
|
||||||
TfhdDefaultBaseIsMoof,
|
TfhdDefaultBaseIsMoof,
|
||||||
)
|
)
|
||||||
m.WriteUint32(tid) // track id
|
m.WriteUint32(tid) // track id
|
||||||
m.WriteUint32(duration) // default sample duration
|
m.WriteUint32(duration) // default sample duration
|
||||||
m.WriteUint32(size) // default sample size
|
m.WriteUint32(size) // default sample size
|
||||||
m.WriteUint32(0x2000000) // default sample flags
|
m.WriteUint32(flags) // default sample flags
|
||||||
m.EndAtom()
|
m.EndAtom()
|
||||||
|
|
||||||
m.StartAtom(MoofTrafTfdt)
|
m.StartAtom(MoofTrafTfdt)
|
||||||
@@ -314,5 +324,4 @@ func (m *Movie) WriteData(b []byte) {
|
|||||||
m.StartAtom(Mdat)
|
m.StartAtom(Mdat)
|
||||||
m.Write(b)
|
m.Write(b)
|
||||||
m.EndAtom()
|
m.EndAtom()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-2
@@ -2,6 +2,7 @@ package iso
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/pcm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) {
|
func (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) {
|
||||||
@@ -46,9 +47,11 @@ func (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) {
|
|||||||
func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, conf []byte) {
|
func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, conf []byte) {
|
||||||
switch codec {
|
switch codec {
|
||||||
case core.CodecAAC, core.CodecMP3:
|
case core.CodecAAC, core.CodecMP3:
|
||||||
m.StartAtom("mp4a")
|
m.StartAtom("mp4a") // supported in all players and browsers
|
||||||
|
case core.CodecFLAC:
|
||||||
|
m.StartAtom("fLaC") // supported in all players and browsers
|
||||||
case core.CodecOpus:
|
case core.CodecOpus:
|
||||||
m.StartAtom("Opus")
|
m.StartAtom("Opus") // supported in Chrome and Firefox
|
||||||
case core.CodecPCMU:
|
case core.CodecPCMU:
|
||||||
m.StartAtom("ulaw")
|
m.StartAtom("ulaw")
|
||||||
case core.CodecPCMA:
|
case core.CodecPCMA:
|
||||||
@@ -56,6 +59,11 @@ func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, con
|
|||||||
default:
|
default:
|
||||||
panic("unsupported iso audio: " + codec)
|
panic("unsupported iso audio: " + codec)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if channels == 0 {
|
||||||
|
channels = 1
|
||||||
|
}
|
||||||
|
|
||||||
m.Skip(6)
|
m.Skip(6)
|
||||||
m.WriteUint16(1) // data_reference_index
|
m.WriteUint16(1) // data_reference_index
|
||||||
m.Skip(2) // version
|
m.Skip(2) // version
|
||||||
@@ -72,6 +80,10 @@ func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, con
|
|||||||
m.WriteEsdsAAC(conf)
|
m.WriteEsdsAAC(conf)
|
||||||
case core.CodecMP3:
|
case core.CodecMP3:
|
||||||
m.WriteEsdsMP3()
|
m.WriteEsdsMP3()
|
||||||
|
case core.CodecFLAC:
|
||||||
|
m.StartAtom("dfLa")
|
||||||
|
m.Write(pcm.FLACHeader(false, sampleRate))
|
||||||
|
m.EndAtom()
|
||||||
case core.CodecOpus:
|
case core.CodecOpus:
|
||||||
// don't know what means this magic
|
// don't know what means this magic
|
||||||
m.StartAtom("dOps")
|
m.StartAtom("dOps")
|
||||||
@@ -106,6 +118,7 @@ func (m *Movie) WriteEsdsAAC(conf []byte) {
|
|||||||
m.Skip(2) // es id
|
m.Skip(2) // es id
|
||||||
m.Skip(1) // es flags
|
m.Skip(1) // es flags
|
||||||
|
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/medfound/mpeg-4-file-sink#aac-audio
|
||||||
m.WriteBytes(4, 0x80, 0x80, 0x80, size4+header+size5)
|
m.WriteBytes(4, 0x80, 0x80, 0x80, size4+header+size5)
|
||||||
m.WriteBytes(0x40) // object id
|
m.WriteBytes(0x40) // object id
|
||||||
m.WriteBytes(0x15) // stream type
|
m.WriteBytes(0x15) // stream type
|
||||||
@@ -139,6 +152,7 @@ func (m *Movie) WriteEsdsMP3() {
|
|||||||
m.Skip(2) // es id
|
m.Skip(2) // es id
|
||||||
m.Skip(1) // es flags
|
m.Skip(1) // es flags
|
||||||
|
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/medfound/mpeg-4-file-sink#mp3-audio
|
||||||
m.WriteBytes(4, 0x80, 0x80, 0x80, size4)
|
m.WriteBytes(4, 0x80, 0x80, 0x80, size4)
|
||||||
m.WriteBytes(0x6B) // object id
|
m.WriteBytes(0x6B) // object id
|
||||||
m.WriteBytes(0x15) // stream type
|
m.WriteBytes(0x15) // stream type
|
||||||
|
|||||||
+40
-32
@@ -6,7 +6,9 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/pcm"
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Consumer struct {
|
type Consumer struct {
|
||||||
@@ -19,6 +21,7 @@ type Consumer struct {
|
|||||||
senders []*core.Sender
|
senders []*core.Sender
|
||||||
|
|
||||||
muxer *Muxer
|
muxer *Muxer
|
||||||
|
mu sync.Mutex
|
||||||
wait byte
|
wait byte
|
||||||
|
|
||||||
send int
|
send int
|
||||||
@@ -52,7 +55,8 @@ func (c *Consumer) GetMedias() []*core.Media {
|
|||||||
func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
|
func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
|
||||||
trackID := byte(len(c.senders))
|
trackID := byte(len(c.senders))
|
||||||
|
|
||||||
handler := core.NewSender(media, track.Codec)
|
codec := track.Codec.Clone()
|
||||||
|
handler := core.NewSender(media, codec)
|
||||||
|
|
||||||
switch track.Codec.Name {
|
switch track.Codec.Name {
|
||||||
case core.CodecH264:
|
case core.CodecH264:
|
||||||
@@ -70,10 +74,12 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv
|
|||||||
c.wait = waitNone
|
c.wait = waitNone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// important to use Mutex because right fragment order
|
||||||
|
c.mu.Lock()
|
||||||
buf := c.muxer.Marshal(trackID, packet)
|
buf := c.muxer.Marshal(trackID, packet)
|
||||||
c.Fire(buf)
|
c.Fire(buf)
|
||||||
|
|
||||||
c.send += len(buf)
|
c.send += len(buf)
|
||||||
|
c.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
if track.Codec.IsRTP() {
|
if track.Codec.IsRTP() {
|
||||||
@@ -97,46 +103,48 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv
|
|||||||
c.wait = waitNone
|
c.wait = waitNone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
buf := c.muxer.Marshal(trackID, packet)
|
buf := c.muxer.Marshal(trackID, packet)
|
||||||
c.Fire(buf)
|
c.Fire(buf)
|
||||||
|
|
||||||
c.send += len(buf)
|
c.send += len(buf)
|
||||||
|
c.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
if track.Codec.IsRTP() {
|
if track.Codec.IsRTP() {
|
||||||
handler.Handler = h265.RTPDepay(track.Codec, handler.Handler)
|
handler.Handler = h265.RTPDepay(track.Codec, handler.Handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
case core.CodecAAC:
|
|
||||||
handler.Handler = func(packet *rtp.Packet) {
|
|
||||||
if c.wait != waitNone {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := c.muxer.Marshal(trackID, packet)
|
|
||||||
c.Fire(buf)
|
|
||||||
|
|
||||||
c.send += len(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
if track.Codec.IsRTP() {
|
|
||||||
handler.Handler = aac.RTPDepay(handler.Handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
case core.CodecOpus, core.CodecMP3, core.CodecPCMU, core.CodecPCMA:
|
|
||||||
handler.Handler = func(packet *rtp.Packet) {
|
|
||||||
if c.wait != waitNone {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := c.muxer.Marshal(trackID, packet)
|
|
||||||
c.Fire(buf)
|
|
||||||
|
|
||||||
c.send += len(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
panic("unsupported codec")
|
handler.Handler = func(packet *rtp.Packet) {
|
||||||
|
if c.wait != waitNone {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
buf := c.muxer.Marshal(trackID, packet)
|
||||||
|
c.Fire(buf)
|
||||||
|
c.send += len(buf)
|
||||||
|
c.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch track.Codec.Name {
|
||||||
|
case core.CodecAAC:
|
||||||
|
if track.Codec.IsRTP() {
|
||||||
|
handler.Handler = aac.RTPDepay(handler.Handler)
|
||||||
|
}
|
||||||
|
case core.CodecOpus, core.CodecMP3: // no changes
|
||||||
|
case core.CodecPCMA, core.CodecPCMU, core.CodecPCM:
|
||||||
|
handler.Handler = pcm.FLACEncoder(track.Codec, handler.Handler)
|
||||||
|
codec.Name = core.CodecFLAC
|
||||||
|
|
||||||
|
default:
|
||||||
|
handler.Handler = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if handler.Handler == nil {
|
||||||
|
println("ERROR: MP4 unsupported codec: " + track.Codec.String())
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
handler.HandleRTP(track)
|
handler.HandleRTP(track)
|
||||||
|
|||||||
+39
-3
@@ -4,9 +4,45 @@ import "github.com/AlexxIT/go2rtc/pkg/core"
|
|||||||
|
|
||||||
// ParseQuery - like usual parse, but with mp4 param handler
|
// ParseQuery - like usual parse, but with mp4 param handler
|
||||||
func ParseQuery(query map[string][]string) []*core.Media {
|
func ParseQuery(query map[string][]string) []*core.Media {
|
||||||
if query["mp4"] != nil {
|
if v := query["mp4"]; len(v) != 0 {
|
||||||
cons := Consumer{}
|
medias := []*core.Media{
|
||||||
return cons.GetMedias()
|
{
|
||||||
|
Kind: core.KindVideo,
|
||||||
|
Direction: core.DirectionSendonly,
|
||||||
|
Codecs: []*core.Codec{
|
||||||
|
{Name: core.CodecH264},
|
||||||
|
{Name: core.CodecH265},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Kind: core.KindAudio,
|
||||||
|
Direction: core.DirectionSendonly,
|
||||||
|
Codecs: []*core.Codec{
|
||||||
|
{Name: core.CodecAAC},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if v[0] == "" {
|
||||||
|
return medias // legacy
|
||||||
|
}
|
||||||
|
|
||||||
|
medias[1].Codecs = append(medias[1].Codecs,
|
||||||
|
&core.Codec{Name: core.CodecPCMA},
|
||||||
|
&core.Codec{Name: core.CodecPCMU},
|
||||||
|
&core.Codec{Name: core.CodecPCM},
|
||||||
|
)
|
||||||
|
|
||||||
|
if v[0] == "flac" {
|
||||||
|
return medias // modern browsers
|
||||||
|
}
|
||||||
|
|
||||||
|
medias[1].Codecs = append(medias[1].Codecs,
|
||||||
|
&core.Codec{Name: core.CodecOpus},
|
||||||
|
&core.Codec{Name: core.CodecMP3},
|
||||||
|
)
|
||||||
|
|
||||||
|
return medias // Chrome, FFmpeg, VLC
|
||||||
}
|
}
|
||||||
|
|
||||||
return core.ParseQuery(query)
|
return core.ParseQuery(query)
|
||||||
|
|||||||
+44
-18
@@ -15,12 +15,14 @@ type Muxer struct {
|
|||||||
fragIndex uint32
|
fragIndex uint32
|
||||||
dts []uint64
|
dts []uint64
|
||||||
pts []uint32
|
pts []uint32
|
||||||
|
codecs []*core.Codec
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MimeH264 = "avc1.640029"
|
MimeH264 = "avc1.640029"
|
||||||
MimeH265 = "hvc1.1.6.L153.B0"
|
MimeH265 = "hvc1.1.6.L153.B0"
|
||||||
MimeAAC = "mp4a.40.2"
|
MimeAAC = "mp4a.40.2"
|
||||||
|
MimeFlac = "flac"
|
||||||
MimeOpus = "opus"
|
MimeOpus = "opus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -43,6 +45,8 @@ func (m *Muxer) MimeCodecs(codecs []*core.Codec) string {
|
|||||||
s += MimeAAC
|
s += MimeAAC
|
||||||
case core.CodecOpus:
|
case core.CodecOpus:
|
||||||
s += MimeOpus
|
s += MimeOpus
|
||||||
|
case core.CodecFLAC:
|
||||||
|
s += MimeFlac
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,14 +112,15 @@ func (m *Muxer) GetInit(codecs []*core.Codec) ([]byte, error) {
|
|||||||
uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, b,
|
uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, b,
|
||||||
)
|
)
|
||||||
|
|
||||||
case core.CodecOpus, core.CodecMP3, core.CodecPCMU, core.CodecPCMA:
|
case core.CodecOpus, core.CodecMP3, core.CodecPCMA, core.CodecPCMU, core.CodecPCM, core.CodecFLAC:
|
||||||
mv.WriteAudioTrack(
|
mv.WriteAudioTrack(
|
||||||
uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, nil,
|
uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, nil,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.pts = append(m.pts, 0)
|
|
||||||
m.dts = append(m.dts, 0)
|
m.dts = append(m.dts, 0)
|
||||||
|
m.pts = append(m.pts, 0)
|
||||||
|
m.codecs = append(m.codecs, codec)
|
||||||
}
|
}
|
||||||
|
|
||||||
mv.StartAtom(iso.MoovMvex)
|
mv.StartAtom(iso.MoovMvex)
|
||||||
@@ -138,28 +143,49 @@ func (m *Muxer) Reset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Muxer) Marshal(trackID byte, packet *rtp.Packet) []byte {
|
func (m *Muxer) Marshal(trackID byte, packet *rtp.Packet) []byte {
|
||||||
// important before increment
|
codec := m.codecs[trackID]
|
||||||
time := m.dts[trackID]
|
|
||||||
|
duration := packet.Timestamp - m.pts[trackID]
|
||||||
|
m.pts[trackID] = packet.Timestamp
|
||||||
|
|
||||||
|
// minumum duration important for MSE in Apple Safari
|
||||||
|
if duration == 0 || duration > codec.ClockRate {
|
||||||
|
duration = codec.ClockRate/1000 + 1
|
||||||
|
m.pts[trackID] += duration
|
||||||
|
}
|
||||||
|
|
||||||
|
size := len(packet.Payload)
|
||||||
|
|
||||||
|
// flags important for Apple Finder video preview
|
||||||
|
var flags uint32
|
||||||
|
switch codec.Name {
|
||||||
|
case core.CodecH264:
|
||||||
|
if h264.IsKeyframe(packet.Payload) {
|
||||||
|
flags = iso.SampleVideoIFrame
|
||||||
|
} else {
|
||||||
|
flags = iso.SampleVideoNonIFrame
|
||||||
|
}
|
||||||
|
case core.CodecH265:
|
||||||
|
if h265.IsKeyframe(packet.Payload) {
|
||||||
|
flags = iso.SampleVideoIFrame
|
||||||
|
} else {
|
||||||
|
flags = iso.SampleVideoNonIFrame
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
flags = iso.SampleAudio // not important
|
||||||
|
}
|
||||||
|
|
||||||
m.fragIndex++
|
m.fragIndex++
|
||||||
|
|
||||||
var duration uint32
|
mv := iso.NewMovie(1024 + size)
|
||||||
newTime := packet.Timestamp
|
|
||||||
if m.pts[trackID] > 0 {
|
|
||||||
duration = newTime - m.pts[trackID]
|
|
||||||
m.dts[trackID] += uint64(duration)
|
|
||||||
} else {
|
|
||||||
// important, or Safari will fail with first frame
|
|
||||||
duration = 1
|
|
||||||
}
|
|
||||||
m.pts[trackID] = newTime
|
|
||||||
|
|
||||||
mv := iso.NewMovie(1024 + len(packet.Payload))
|
|
||||||
mv.WriteMovieFragment(
|
mv.WriteMovieFragment(
|
||||||
m.fragIndex, uint32(trackID+1), duration,
|
m.fragIndex, uint32(trackID+1), duration, uint32(size), flags, m.dts[trackID],
|
||||||
uint32(len(packet.Payload)), time,
|
|
||||||
)
|
)
|
||||||
mv.WriteData(packet.Payload)
|
mv.WriteData(packet.Payload)
|
||||||
|
|
||||||
|
//log.Printf("[MP4] track=%d ts=%6d dur=%5d idx=%3d len=%d", trackID+1, m.dts[trackID], duration, m.fragIndex, len(packet.Payload))
|
||||||
|
|
||||||
|
m.dts[trackID] += uint64(duration)
|
||||||
|
|
||||||
return mv.Bytes()
|
return mv.Bytes()
|
||||||
}
|
}
|
||||||
|
|||||||
+146
@@ -0,0 +1,146 @@
|
|||||||
|
// Package pcm - support raw (verbatim) PCM 16 bit in the FLAC container:
|
||||||
|
// - only 1 channel
|
||||||
|
// - only 16 bit per sample
|
||||||
|
// - only 8000, 16000, 24000, 48000 sample rate
|
||||||
|
package pcm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
"github.com/sigurn/crc16"
|
||||||
|
"github.com/sigurn/crc8"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FLACHeader(magic bool, sampleRate uint32) []byte {
|
||||||
|
b := make([]byte, 42)
|
||||||
|
|
||||||
|
if magic {
|
||||||
|
copy(b, "fLaC") // [0..3]
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://xiph.org/flac/format.html#metadata_block_header
|
||||||
|
b[4] = 0x80 // [4] lastMetadata=1 (1 bit), blockType=0 - STREAMINFO (7 bit)
|
||||||
|
b[7] = 0x22 // [5..7] blockLength=34 (24 bit)
|
||||||
|
|
||||||
|
// Important for Apple QuickTime player:
|
||||||
|
// 1. Both values should be same
|
||||||
|
// 2. Maximum value = 32768
|
||||||
|
binary.BigEndian.PutUint16(b[8:], 32768) // [8..9] info.BlockSizeMin=16 (16 bit)
|
||||||
|
binary.BigEndian.PutUint16(b[10:], 32768) // [10..11] info.BlockSizeMin=65535 (16 bit)
|
||||||
|
|
||||||
|
// [12..14] info.FrameSizeMin=0 (24 bit)
|
||||||
|
// [15..17] info.FrameSizeMax=0 (24 bit)
|
||||||
|
|
||||||
|
b[18] = byte(sampleRate >> 12)
|
||||||
|
b[19] = byte(sampleRate >> 4)
|
||||||
|
b[20] = byte(sampleRate << 4) // [18..20] info.SampleRate=8000 (20 bit), info.NChannels=1-1 (3 bit)
|
||||||
|
|
||||||
|
b[21] = 0xF0 // [21..25] info.BitsPerSample=16-1 (5 bit), info.NSamples (36 bit)
|
||||||
|
|
||||||
|
// [26..41] MD5sum (16 bytes)
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
var table8 *crc8.Table
|
||||||
|
var table16 *crc16.Table
|
||||||
|
|
||||||
|
func FLACEncoder(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
|
||||||
|
if codec.Channels >= 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var sr byte
|
||||||
|
switch codec.ClockRate {
|
||||||
|
case 8000:
|
||||||
|
sr = 0b0100
|
||||||
|
case 16000:
|
||||||
|
sr = 0b0101
|
||||||
|
case 22050:
|
||||||
|
sr = 0b0110
|
||||||
|
case 24000:
|
||||||
|
sr = 0b0111
|
||||||
|
case 32000:
|
||||||
|
sr = 0b1000
|
||||||
|
case 44100:
|
||||||
|
sr = 0b1001
|
||||||
|
case 48000:
|
||||||
|
sr = 0b1010
|
||||||
|
case 96000:
|
||||||
|
sr = 0b1011
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if table8 == nil {
|
||||||
|
table8 = crc8.MakeTable(crc8.CRC8)
|
||||||
|
}
|
||||||
|
if table16 == nil {
|
||||||
|
table16 = crc16.MakeTable(crc16.CRC16_BUYPASS)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sampleNumber int32
|
||||||
|
|
||||||
|
return func(packet *rtp.Packet) {
|
||||||
|
samples := uint16(len(packet.Payload))
|
||||||
|
|
||||||
|
if codec.Name == core.CodecPCM {
|
||||||
|
samples /= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://xiph.org/flac/format.html#frame_header
|
||||||
|
buf := make([]byte, samples*2+30)
|
||||||
|
|
||||||
|
// 1. Frame header
|
||||||
|
buf[0] = 0xFF
|
||||||
|
buf[1] = 0xF9 // [0..1] syncCode=0xFFF8 - reserved (15 bit), blockStrategy=1 - variable-blocksize (1 bit)
|
||||||
|
buf[2] = 0x70 | sr // blockSizeType=7 (4 bit), sampleRate=4 - 8000 (4 bit)
|
||||||
|
buf[3] = 0x08 // channels=1-1 (4 bit), sampleSize=4 - 16 (3 bit), reserved=0 (1 bit)
|
||||||
|
|
||||||
|
n := 4 + utf8.EncodeRune(buf[4:], sampleNumber) // 4 bytes max
|
||||||
|
sampleNumber += int32(samples)
|
||||||
|
|
||||||
|
// this is wrong but very simple frame block size value
|
||||||
|
binary.BigEndian.PutUint16(buf[n:], samples-1)
|
||||||
|
n += 2
|
||||||
|
|
||||||
|
buf[n] = crc8.Checksum(buf[:n], table8)
|
||||||
|
n += 1
|
||||||
|
|
||||||
|
// 2. Subframe header
|
||||||
|
buf[n] = 0x02 // padding=0 (1 bit), subframeType=1 - verbatim (6 bit), wastedFlag=0 (1 bit)
|
||||||
|
n += 1
|
||||||
|
|
||||||
|
// 3. Subframe
|
||||||
|
switch codec.Name {
|
||||||
|
case core.CodecPCMA:
|
||||||
|
for _, b := range packet.Payload {
|
||||||
|
s16 := PCMAtoPCM(b)
|
||||||
|
buf[n] = byte(s16 >> 8)
|
||||||
|
buf[n+1] = byte(s16)
|
||||||
|
n += 2
|
||||||
|
}
|
||||||
|
case core.CodecPCMU:
|
||||||
|
for _, b := range packet.Payload {
|
||||||
|
s16 := PCMUtoPCM(b)
|
||||||
|
buf[n] = byte(s16 >> 8)
|
||||||
|
buf[n+1] = byte(s16)
|
||||||
|
n += 2
|
||||||
|
}
|
||||||
|
case core.CodecPCM:
|
||||||
|
n += copy(buf[n:], packet.Payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Frame footer
|
||||||
|
crc := crc16.Checksum(buf[:n], table16)
|
||||||
|
binary.BigEndian.PutUint16(buf[n:], crc)
|
||||||
|
n += 2
|
||||||
|
|
||||||
|
clone := *packet
|
||||||
|
clone.Payload = buf[:n]
|
||||||
|
|
||||||
|
handler(&clone)
|
||||||
|
}
|
||||||
|
}
|
||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
package pcm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Resample(codec *core.Codec, sampleRate uint32, handler core.HandlerFunc) core.HandlerFunc {
|
||||||
|
n := float32(codec.ClockRate) / float32(sampleRate)
|
||||||
|
|
||||||
|
switch codec.Name {
|
||||||
|
case core.CodecPCMA:
|
||||||
|
return DownsampleByte(PCMAtoPCM, PCMtoPCMA, n, handler)
|
||||||
|
case core.CodecPCMU:
|
||||||
|
return DownsampleByte(PCMUtoPCM, PCMtoPCMU, n, handler)
|
||||||
|
case core.CodecPCM:
|
||||||
|
if n == 1 {
|
||||||
|
return ResamplePCM(PCMtoPCMA, handler)
|
||||||
|
}
|
||||||
|
return DownsamplePCM(PCMtoPCMA, n, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
panic(core.Caller())
|
||||||
|
}
|
||||||
|
|
||||||
|
func DownsampleByte(
|
||||||
|
toPCM func(byte) int16, fromPCM func(int16) byte, n float32, handler core.HandlerFunc,
|
||||||
|
) core.HandlerFunc {
|
||||||
|
var sampleN, sampleSum float32
|
||||||
|
var ts uint32
|
||||||
|
|
||||||
|
return func(packet *rtp.Packet) {
|
||||||
|
samples := len(packet.Payload)
|
||||||
|
newLen := uint32((float32(samples) + sampleN) / n)
|
||||||
|
|
||||||
|
oldSamples := packet.Payload
|
||||||
|
newSamples := make([]byte, newLen)
|
||||||
|
|
||||||
|
var i int
|
||||||
|
for _, sample := range oldSamples {
|
||||||
|
sampleSum += float32(toPCM(sample))
|
||||||
|
if sampleN++; sampleN >= n {
|
||||||
|
newSamples[i] = fromPCM(int16(sampleSum / n))
|
||||||
|
i++
|
||||||
|
|
||||||
|
sampleSum = 0
|
||||||
|
sampleN -= n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ts += newLen
|
||||||
|
|
||||||
|
clone := *packet
|
||||||
|
clone.Payload = newSamples
|
||||||
|
clone.Timestamp = ts
|
||||||
|
handler(&clone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResamplePCM(fromPCM func(int16) byte, handler core.HandlerFunc) core.HandlerFunc {
|
||||||
|
var ts uint32
|
||||||
|
|
||||||
|
return func(packet *rtp.Packet) {
|
||||||
|
len1 := len(packet.Payload)
|
||||||
|
len2 := len1 / 2
|
||||||
|
|
||||||
|
oldSamples := packet.Payload
|
||||||
|
newSamples := make([]byte, len2)
|
||||||
|
|
||||||
|
var i2 int
|
||||||
|
for i1 := 0; i1 < len1; i1 += 2 {
|
||||||
|
sample := int16(uint16(oldSamples[i1])<<8 | uint16(oldSamples[i1+1]))
|
||||||
|
newSamples[i2] = fromPCM(sample)
|
||||||
|
i2++
|
||||||
|
}
|
||||||
|
|
||||||
|
ts += uint32(len2)
|
||||||
|
|
||||||
|
clone := *packet
|
||||||
|
clone.Payload = newSamples
|
||||||
|
clone.Timestamp = ts
|
||||||
|
handler(&clone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DownsamplePCM(fromPCM func(int16) byte, n float32, handler core.HandlerFunc) core.HandlerFunc {
|
||||||
|
var sampleN, sampleSum float32
|
||||||
|
var ts uint32
|
||||||
|
|
||||||
|
return func(packet *rtp.Packet) {
|
||||||
|
samples := len(packet.Payload) / 2
|
||||||
|
newLen := uint32((float32(samples) + sampleN) / n)
|
||||||
|
|
||||||
|
oldSamples := packet.Payload
|
||||||
|
newSamples := make([]byte, newLen)
|
||||||
|
|
||||||
|
var i2 int
|
||||||
|
for i1 := 0; i1 < len(packet.Payload); i1 += 2 {
|
||||||
|
sampleSum += float32(int16(uint16(oldSamples[i1])<<8 | uint16(oldSamples[i1+1])))
|
||||||
|
if sampleN++; sampleN >= n {
|
||||||
|
newSamples[i2] = fromPCM(int16(sampleSum / n))
|
||||||
|
i2++
|
||||||
|
|
||||||
|
sampleSum = 0
|
||||||
|
sampleN -= n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ts += newLen
|
||||||
|
|
||||||
|
clone := *packet
|
||||||
|
clone.Payload = newSamples
|
||||||
|
clone.Timestamp = ts
|
||||||
|
handler(&clone)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// Package pcm
|
||||||
|
// https://www.codeproject.com/Articles/14237/Using-the-G711-standard
|
||||||
|
package pcm
|
||||||
|
|
||||||
|
const alawMax = 0x7FFF
|
||||||
|
|
||||||
|
func PCMAtoPCM(alaw byte) int16 {
|
||||||
|
alaw ^= 0xD5
|
||||||
|
|
||||||
|
data := int16(((alaw & 0x0F) << 4) + 8)
|
||||||
|
exponent := (alaw & 0x70) >> 4
|
||||||
|
|
||||||
|
if exponent != 0 {
|
||||||
|
data |= 0x100
|
||||||
|
}
|
||||||
|
|
||||||
|
if exponent > 1 {
|
||||||
|
data <<= exponent - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// sign
|
||||||
|
if alaw&0x80 == 0 {
|
||||||
|
return data
|
||||||
|
} else {
|
||||||
|
return -data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PCMtoPCMA(pcm int16) byte {
|
||||||
|
var alaw byte
|
||||||
|
|
||||||
|
if pcm < 0 {
|
||||||
|
pcm = -pcm
|
||||||
|
alaw = 0x80
|
||||||
|
}
|
||||||
|
|
||||||
|
if pcm > alawMax {
|
||||||
|
pcm = alawMax
|
||||||
|
}
|
||||||
|
|
||||||
|
exponent := byte(7)
|
||||||
|
for expMask := int16(0x4000); (pcm&expMask) == 0 && exponent > 0; expMask >>= 1 {
|
||||||
|
exponent--
|
||||||
|
}
|
||||||
|
|
||||||
|
if exponent == 0 {
|
||||||
|
alaw |= byte(pcm>>4) & 0x0F
|
||||||
|
} else {
|
||||||
|
alaw |= (exponent << 4) | (byte(pcm>>(exponent+3)) & 0x0F)
|
||||||
|
}
|
||||||
|
|
||||||
|
return alaw ^ 0xD5
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// Package pcm
|
||||||
|
// https://www.codeproject.com/Articles/14237/Using-the-G711-standard
|
||||||
|
package pcm
|
||||||
|
|
||||||
|
const bias = 0x84 // 132 or 1000 0100
|
||||||
|
const ulawMax = alawMax - bias
|
||||||
|
|
||||||
|
func PCMUtoPCM(ulaw byte) int16 {
|
||||||
|
ulaw = ^ulaw
|
||||||
|
|
||||||
|
exponent := (ulaw & 0x70) >> 4
|
||||||
|
data := (int16((((ulaw&0x0F)|0x10)<<1)+1) << (exponent + 2)) - bias
|
||||||
|
|
||||||
|
// sign
|
||||||
|
if ulaw&0x80 == 0 {
|
||||||
|
return data
|
||||||
|
} else if data == 0 {
|
||||||
|
return -1
|
||||||
|
} else {
|
||||||
|
return -data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PCMtoPCMU(pcm int16) byte {
|
||||||
|
var ulaw byte
|
||||||
|
|
||||||
|
if pcm < 0 {
|
||||||
|
pcm = -pcm
|
||||||
|
ulaw = 0x80
|
||||||
|
}
|
||||||
|
|
||||||
|
if pcm > ulawMax {
|
||||||
|
pcm = ulawMax
|
||||||
|
}
|
||||||
|
|
||||||
|
pcm += bias
|
||||||
|
|
||||||
|
exponent := byte(7)
|
||||||
|
for expMask := int16(0x4000); (pcm & expMask) == 0; expMask >>= 1 {
|
||||||
|
exponent--
|
||||||
|
}
|
||||||
|
|
||||||
|
// mantisa
|
||||||
|
ulaw |= byte(pcm>>(exponent+3)) & 0x0F
|
||||||
|
|
||||||
|
if exponent > 0 {
|
||||||
|
ulaw |= exponent << 4
|
||||||
|
}
|
||||||
|
|
||||||
|
return ^ulaw
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
// Package v1
|
||||||
|
// http://web.archive.org/web/20110719132013/http://hazelware.luggle.com/tutorials/mulawcompression.html
|
||||||
|
package v1
|
||||||
|
|
||||||
|
const cBias = 0x84
|
||||||
|
const cClip = 32635
|
||||||
|
|
||||||
|
var MuLawCompressTable = [256]byte{
|
||||||
|
0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3,
|
||||||
|
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||||
|
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
|
||||||
|
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
|
||||||
|
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
|
||||||
|
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
|
||||||
|
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
|
||||||
|
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
|
||||||
|
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
||||||
|
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
||||||
|
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
||||||
|
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
||||||
|
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
||||||
|
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
||||||
|
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
||||||
|
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
||||||
|
}
|
||||||
|
|
||||||
|
func LinearToMuLawSample(sample int16) byte {
|
||||||
|
sign := byte(sample>>8) & 0x80
|
||||||
|
if sign != 0 {
|
||||||
|
sample = -sample
|
||||||
|
}
|
||||||
|
|
||||||
|
if sample > cClip {
|
||||||
|
sample = cClip
|
||||||
|
}
|
||||||
|
sample = sample + cBias
|
||||||
|
|
||||||
|
exponent := MuLawCompressTable[(sample>>7)&0xFF]
|
||||||
|
mantissa := byte(sample>>(exponent+3)) & 0x0F
|
||||||
|
|
||||||
|
compressedByte := ^(sign | (exponent << 4) | mantissa)
|
||||||
|
|
||||||
|
return compressedByte
|
||||||
|
}
|
||||||
|
|
||||||
|
var ALawCompressTable = [128]byte{
|
||||||
|
1, 1, 2, 2, 3, 3, 3, 3,
|
||||||
|
4, 4, 4, 4, 4, 4, 4, 4,
|
||||||
|
5, 5, 5, 5, 5, 5, 5, 5,
|
||||||
|
5, 5, 5, 5, 5, 5, 5, 5,
|
||||||
|
6, 6, 6, 6, 6, 6, 6, 6,
|
||||||
|
6, 6, 6, 6, 6, 6, 6, 6,
|
||||||
|
6, 6, 6, 6, 6, 6, 6, 6,
|
||||||
|
6, 6, 6, 6, 6, 6, 6, 6,
|
||||||
|
7, 7, 7, 7, 7, 7, 7, 7,
|
||||||
|
7, 7, 7, 7, 7, 7, 7, 7,
|
||||||
|
7, 7, 7, 7, 7, 7, 7, 7,
|
||||||
|
7, 7, 7, 7, 7, 7, 7, 7,
|
||||||
|
7, 7, 7, 7, 7, 7, 7, 7,
|
||||||
|
7, 7, 7, 7, 7, 7, 7, 7,
|
||||||
|
7, 7, 7, 7, 7, 7, 7, 7,
|
||||||
|
7, 7, 7, 7, 7, 7, 7, 7,
|
||||||
|
}
|
||||||
|
|
||||||
|
func LinearToALawSample(sample int16) byte {
|
||||||
|
sign := byte((^sample)>>8) & 0x80
|
||||||
|
if sign == 0 {
|
||||||
|
sample = -sample
|
||||||
|
}
|
||||||
|
|
||||||
|
if sample > cClip {
|
||||||
|
sample = cClip
|
||||||
|
}
|
||||||
|
|
||||||
|
var compressedByte byte
|
||||||
|
if sample >= 256 {
|
||||||
|
exponent := ALawCompressTable[(sample>>8)&0x7F]
|
||||||
|
mantissa := byte(sample>>(exponent+3)) & 0x0F
|
||||||
|
compressedByte = (exponent << 4) | mantissa
|
||||||
|
} else {
|
||||||
|
compressedByte = byte(sample >> 4)
|
||||||
|
}
|
||||||
|
compressedByte ^= sign ^ 0x55
|
||||||
|
return compressedByte
|
||||||
|
}
|
||||||
|
|
||||||
|
var MuLawDecompressTable = [256]int16{
|
||||||
|
-32124, -31100, -30076, -29052, -28028, -27004, -25980, -24956,
|
||||||
|
-23932, -22908, -21884, -20860, -19836, -18812, -17788, -16764,
|
||||||
|
-15996, -15484, -14972, -14460, -13948, -13436, -12924, -12412,
|
||||||
|
-11900, -11388, -10876, -10364, -9852, -9340, -8828, -8316,
|
||||||
|
-7932, -7676, -7420, -7164, -6908, -6652, -6396, -6140,
|
||||||
|
-5884, -5628, -5372, -5116, -4860, -4604, -4348, -4092,
|
||||||
|
-3900, -3772, -3644, -3516, -3388, -3260, -3132, -3004,
|
||||||
|
-2876, -2748, -2620, -2492, -2364, -2236, -2108, -1980,
|
||||||
|
-1884, -1820, -1756, -1692, -1628, -1564, -1500, -1436,
|
||||||
|
-1372, -1308, -1244, -1180, -1116, -1052, -988, -924,
|
||||||
|
-876, -844, -812, -780, -748, -716, -684, -652,
|
||||||
|
-620, -588, -556, -524, -492, -460, -428, -396,
|
||||||
|
-372, -356, -340, -324, -308, -292, -276, -260,
|
||||||
|
-244, -228, -212, -196, -180, -164, -148, -132,
|
||||||
|
-120, -112, -104, -96, -88, -80, -72, -64,
|
||||||
|
-56, -48, -40, -32, -24, -16, -8, -1,
|
||||||
|
32124, 31100, 30076, 29052, 28028, 27004, 25980, 24956,
|
||||||
|
23932, 22908, 21884, 20860, 19836, 18812, 17788, 16764,
|
||||||
|
15996, 15484, 14972, 14460, 13948, 13436, 12924, 12412,
|
||||||
|
11900, 11388, 10876, 10364, 9852, 9340, 8828, 8316,
|
||||||
|
7932, 7676, 7420, 7164, 6908, 6652, 6396, 6140,
|
||||||
|
5884, 5628, 5372, 5116, 4860, 4604, 4348, 4092,
|
||||||
|
3900, 3772, 3644, 3516, 3388, 3260, 3132, 3004,
|
||||||
|
2876, 2748, 2620, 2492, 2364, 2236, 2108, 1980,
|
||||||
|
1884, 1820, 1756, 1692, 1628, 1564, 1500, 1436,
|
||||||
|
1372, 1308, 1244, 1180, 1116, 1052, 988, 924,
|
||||||
|
876, 844, 812, 780, 748, 716, 684, 652,
|
||||||
|
620, 588, 556, 524, 492, 460, 428, 396,
|
||||||
|
372, 356, 340, 324, 308, 292, 276, 260,
|
||||||
|
244, 228, 212, 196, 180, 164, 148, 132,
|
||||||
|
120, 112, 104, 96, 88, 80, 72, 64,
|
||||||
|
56, 48, 40, 32, 24, 16, 8, 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ALawDecompressTable = [256]int16{
|
||||||
|
-5504, -5248, -6016, -5760, -4480, -4224, -4992, -4736,
|
||||||
|
-7552, -7296, -8064, -7808, -6528, -6272, -7040, -6784,
|
||||||
|
-2752, -2624, -3008, -2880, -2240, -2112, -2496, -2368,
|
||||||
|
-3776, -3648, -4032, -3904, -3264, -3136, -3520, -3392,
|
||||||
|
-22016, -20992, -24064, -23040, -17920, -16896, -19968, -18944,
|
||||||
|
-30208, -29184, -32256, -31232, -26112, -25088, -28160, -27136,
|
||||||
|
-11008, -10496, -12032, -11520, -8960, -8448, -9984, -9472,
|
||||||
|
-15104, -14592, -16128, -15616, -13056, -12544, -14080, -13568,
|
||||||
|
-344, -328, -376, -360, -280, -264, -312, -296,
|
||||||
|
-472, -456, -504, -488, -408, -392, -440, -424,
|
||||||
|
-88, -72, -120, -104, -24, -8, -56, -40,
|
||||||
|
-216, -200, -248, -232, -152, -136, -184, -168,
|
||||||
|
-1376, -1312, -1504, -1440, -1120, -1056, -1248, -1184,
|
||||||
|
-1888, -1824, -2016, -1952, -1632, -1568, -1760, -1696,
|
||||||
|
-688, -656, -752, -720, -560, -528, -624, -592,
|
||||||
|
-944, -912, -1008, -976, -816, -784, -880, -848,
|
||||||
|
5504, 5248, 6016, 5760, 4480, 4224, 4992, 4736,
|
||||||
|
7552, 7296, 8064, 7808, 6528, 6272, 7040, 6784,
|
||||||
|
2752, 2624, 3008, 2880, 2240, 2112, 2496, 2368,
|
||||||
|
3776, 3648, 4032, 3904, 3264, 3136, 3520, 3392,
|
||||||
|
22016, 20992, 24064, 23040, 17920, 16896, 19968, 18944,
|
||||||
|
30208, 29184, 32256, 31232, 26112, 25088, 28160, 27136,
|
||||||
|
11008, 10496, 12032, 11520, 8960, 8448, 9984, 9472,
|
||||||
|
15104, 14592, 16128, 15616, 13056, 12544, 14080, 13568,
|
||||||
|
344, 328, 376, 360, 280, 264, 312, 296,
|
||||||
|
472, 456, 504, 488, 408, 392, 440, 424,
|
||||||
|
88, 72, 120, 104, 24, 8, 56, 40,
|
||||||
|
216, 200, 248, 232, 152, 136, 184, 168,
|
||||||
|
1376, 1312, 1504, 1440, 1120, 1056, 1248, 1184,
|
||||||
|
1888, 1824, 2016, 1952, 1632, 1568, 1760, 1696,
|
||||||
|
688, 656, 752, 720, 560, 528, 624, 592,
|
||||||
|
944, 912, 1008, 976, 816, 784, 880, 848,
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
v2 "github.com/AlexxIT/go2rtc/pkg/pcm"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPCMUtoPCM(t *testing.T) {
|
||||||
|
for pcmu := byte(0); pcmu < 255; pcmu++ {
|
||||||
|
pcm1 := MuLawDecompressTable[pcmu]
|
||||||
|
pcm2 := v2.PCMUtoPCM(pcmu)
|
||||||
|
require.Equal(t, pcm1, pcm2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPCMAtoPCM(t *testing.T) {
|
||||||
|
for pcma := byte(0); pcma < 255; pcma++ {
|
||||||
|
pcm1 := ALawDecompressTable[pcma]
|
||||||
|
pcm2 := v2.PCMAtoPCM(pcma)
|
||||||
|
require.Equal(t, pcm1, pcm2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPCMtoPCMU(t *testing.T) {
|
||||||
|
for pcm := int16(-32768); pcm < 32767; pcm++ {
|
||||||
|
pcmu1 := LinearToMuLawSample(pcm)
|
||||||
|
pcmu2 := v2.PCMtoPCMU(pcm)
|
||||||
|
require.Equal(t, pcmu1, pcmu2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPCMtoPCMA(t *testing.T) {
|
||||||
|
for pcm := int16(-32768); pcm < 32767; pcm++ {
|
||||||
|
pcma1 := LinearToALawSample(pcm)
|
||||||
|
pcma2 := v2.PCMtoPCMA(pcm)
|
||||||
|
require.Equal(t, pcma1, pcma2)
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-1
@@ -288,6 +288,8 @@ func (c *Conn) Teardown() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) Close() error {
|
func (c *Conn) Close() error {
|
||||||
_ = c.Teardown()
|
if c.mode == core.ModeActiveProducer {
|
||||||
|
_ = c.Teardown()
|
||||||
|
}
|
||||||
return c.conn.Close()
|
return c.conn.Close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,21 +58,22 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv
|
|||||||
|
|
||||||
// save original codec to sender (can have Codec.Name = ANY)
|
// save original codec to sender (can have Codec.Name = ANY)
|
||||||
sender := core.NewSender(media, codec)
|
sender := core.NewSender(media, codec)
|
||||||
sender.Handler = c.packetWriter(codec, channel)
|
// important to send original codec for valid IsRTP check
|
||||||
|
sender.Handler = c.packetWriter(track.Codec, channel, codec.PayloadType)
|
||||||
sender.HandleRTP(track)
|
sender.HandleRTP(track)
|
||||||
|
|
||||||
c.senders = append(c.senders, sender)
|
c.senders = append(c.senders, sender)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) packetWriter(codec *core.Codec, channel uint8) core.HandlerFunc {
|
func (c *Conn) packetWriter(codec *core.Codec, channel, payloadType uint8) core.HandlerFunc {
|
||||||
handlerFunc := func(packet *rtp.Packet) {
|
handlerFunc := func(packet *rtp.Packet) {
|
||||||
if c.state == StateNone {
|
if c.state == StateNone {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
clone := *packet
|
clone := *packet
|
||||||
clone.Header.PayloadType = codec.PayloadType
|
clone.Header.PayloadType = payloadType
|
||||||
|
|
||||||
size := clone.MarshalSize()
|
size := clone.MarshalSize()
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -40,8 +40,9 @@ func (c *Conn) Accept() error {
|
|||||||
|
|
||||||
if !c.auth.Validate(req) {
|
if !c.auth.Validate(req) {
|
||||||
res := &tcp.Response{
|
res := &tcp.Response{
|
||||||
Status: "401 Unauthorized",
|
Status: "401 Unauthorized",
|
||||||
Header: map[string][]string{"Www-Authenticate": {`Basic realm="go2rtc"`}},
|
Header: map[string][]string{"Www-Authenticate": {`Basic realm="go2rtc"`}},
|
||||||
|
Request: req,
|
||||||
}
|
}
|
||||||
if err = c.WriteResponse(res); err != nil {
|
if err = c.WriteResponse(res); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
+15
-4
@@ -5,11 +5,12 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/pcm"
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Conn) GetMedias() []*core.Media {
|
func (c *Conn) GetMedias() []*core.Media {
|
||||||
return c.medias
|
return WithResampling(c.medias)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||||
@@ -31,15 +32,16 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv
|
|||||||
}
|
}
|
||||||
|
|
||||||
localTrack := c.getTranseiver(media.ID).Sender().Track().(*Track)
|
localTrack := c.getTranseiver(media.ID).Sender().Track().(*Track)
|
||||||
|
payloadType := codec.PayloadType
|
||||||
|
|
||||||
sender := core.NewSender(media, track.Codec)
|
sender := core.NewSender(media, codec)
|
||||||
sender.Handler = func(packet *rtp.Packet) {
|
sender.Handler = func(packet *rtp.Packet) {
|
||||||
c.send += packet.MarshalSize()
|
c.send += packet.MarshalSize()
|
||||||
//important to send with remote PayloadType
|
//important to send with remote PayloadType
|
||||||
_ = localTrack.WriteRTP(codec.PayloadType, packet)
|
_ = localTrack.WriteRTP(payloadType, packet)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch codec.Name {
|
switch track.Codec.Name {
|
||||||
case core.CodecH264:
|
case core.CodecH264:
|
||||||
sender.Handler = h264.RTPPay(1200, sender.Handler)
|
sender.Handler = h264.RTPPay(1200, sender.Handler)
|
||||||
if track.Codec.IsRTP() {
|
if track.Codec.IsRTP() {
|
||||||
@@ -55,6 +57,15 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv
|
|||||||
if track.Codec.IsRTP() {
|
if track.Codec.IsRTP() {
|
||||||
sender.Handler = h265.RTPDepay(track.Codec, sender.Handler)
|
sender.Handler = h265.RTPDepay(track.Codec, sender.Handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case core.CodecPCMA, core.CodecPCMU, core.CodecPCM:
|
||||||
|
if codec.ClockRate == 0 {
|
||||||
|
if codec.Name == core.CodecPCM {
|
||||||
|
codec.Name = core.CodecPCMA
|
||||||
|
}
|
||||||
|
codec.ClockRate = 8000
|
||||||
|
sender.Handler = pcm.Resample(track.Codec, 8000, sender.Handler)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sender.HandleRTP(track)
|
sender.HandleRTP(track)
|
||||||
|
|||||||
@@ -52,6 +52,53 @@ func UnmarshalMedias(descriptions []*sdp.MediaDescription) (medias []*core.Media
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithResampling(medias []*core.Media) []*core.Media {
|
||||||
|
for _, media := range medias {
|
||||||
|
if media.Kind != core.KindAudio || media.Direction != core.DirectionSendonly {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var pcma, pcmu, pcm *core.Codec
|
||||||
|
|
||||||
|
for _, codec := range media.Codecs {
|
||||||
|
switch codec.Name {
|
||||||
|
case core.CodecPCMA:
|
||||||
|
if codec.ClockRate != 0 {
|
||||||
|
pcma = codec
|
||||||
|
} else {
|
||||||
|
pcma = nil
|
||||||
|
}
|
||||||
|
case core.CodecPCMU:
|
||||||
|
if codec.ClockRate != 0 {
|
||||||
|
pcmu = codec
|
||||||
|
} else {
|
||||||
|
pcmu = nil
|
||||||
|
}
|
||||||
|
case core.CodecPCM:
|
||||||
|
pcm = codec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if pcma != nil {
|
||||||
|
pcma = pcma.Clone()
|
||||||
|
pcma.ClockRate = 0 // reset clock rate so will match any
|
||||||
|
media.Codecs = append(media.Codecs, pcma)
|
||||||
|
}
|
||||||
|
if pcmu != nil {
|
||||||
|
pcmu = pcmu.Clone()
|
||||||
|
pcmu.ClockRate = 0
|
||||||
|
media.Codecs = append(media.Codecs, pcmu)
|
||||||
|
}
|
||||||
|
if pcma != nil && pcm == nil {
|
||||||
|
pcm = pcma.Clone()
|
||||||
|
pcm.Name = core.CodecPCM
|
||||||
|
media.Codecs = append(media.Codecs, pcm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return medias
|
||||||
|
}
|
||||||
|
|
||||||
func NewCandidate(network, address string) (string, error) {
|
func NewCandidate(network, address string) (string, error) {
|
||||||
i := strings.LastIndexByte(address, ':')
|
i := strings.LastIndexByte(address, ':')
|
||||||
if i < 0 {
|
if i < 0 {
|
||||||
|
|||||||
+7
-5
@@ -67,12 +67,14 @@
|
|||||||
|
|
||||||
<h2>H264/H265 source</h2>
|
<h2>H264/H265 source</h2>
|
||||||
<li><a href="stream.html?src=${src}&mode=webrtc">stream.html</a> WebRTC stream / browsers: all / codecs: H264, PCMU, PCMA, OPUS / +H265 in Safari</li>
|
<li><a href="stream.html?src=${src}&mode=webrtc">stream.html</a> WebRTC stream / browsers: all / codecs: H264, PCMU, PCMA, OPUS / +H265 in Safari</li>
|
||||||
<li><a href="stream.html?src=${src}&mode=mse">stream.html</a> MSE stream / browsers: Chrome, Firefox, Safari Mac/iPad / codecs: H264, H265*, AAC / +OPUS in Chrome and Firefox</li>
|
<li><a href="stream.html?src=${src}&mode=mse">stream.html</a> MSE stream / browsers: Chrome, Firefox, Safari Mac/iPad / codecs: H264, H265*, AAC, PCMA*, PCMU*, PCM* / +OPUS in Chrome and Firefox</li>
|
||||||
<li><a href="api/stream.mp4?src=${src}">stream.mp4</a> MP4 stream with AAC audio / browsers: Chrome, Firefox / codecs: H264, H265*, AAC</li>
|
<li><a href="api/stream.mp4?src=${src}">stream.mp4</a> legacy MP4 stream with AAC audio / browsers: Chrome, Firefox / codecs: H264, H265*, AAC</li>
|
||||||
<li><a href="api/stream.mp4?src=${src}&video=h264,h265&audio=aac,opus,mp3,pcma,pcmu">stream.mp4</a> MP4 stream with any audio / browsers: Chrome / codecs: H264, H265*, AAC, OPUS, MP3, PCMU, PCMA</li>
|
<li><a href="api/stream.mp4?src=${src}&mp4=flac">stream.mp4</a> modern MP4 stream with common audio / browsers: Chrome, Firefox / codecs: H264, H265*, AAC, FLAC (PCMA, PCMU, PCM)</li>
|
||||||
|
<li><a href="api/stream.mp4?src=${src}&mp4=all">stream.mp4</a> MP4 stream with any audio / browsers: Chrome / codecs: H264, H265*, AAC, OPUS, MP3, FLAC (PCMA, PCMU, PCM)</li>
|
||||||
<li><a href="api/frame.mp4?src=${src}">frame.mp4</a> snapshot in MP4-format / browsers: all / codecs: H264, H265*</li>
|
<li><a href="api/frame.mp4?src=${src}">frame.mp4</a> snapshot in MP4-format / browsers: all / codecs: H264, H265*</li>
|
||||||
<li><a href="api/stream.m3u8?src=${src}">stream.m3u8</a> HLS/TS / browsers: Safari all, Chrome Android / codecs: H264</li>
|
<li><a href="api/stream.m3u8?src=${src}">stream.m3u8</a> legacy HLS/TS / browsers: Safari all, Chrome Android / codecs: H264</li>
|
||||||
<li><a href="api/stream.m3u8?src=${src}&mp4">stream.m3u8</a> HLS/fMP4 / browsers: Safari all, Chrome Android / codecs: H264, H265*, AAC</li>
|
<li><a href="api/stream.m3u8?src=${src}&mp4">stream.m3u8</a> legacy HLS/fMP4 / browsers: Safari all, Chrome Android / codecs: H264, H265*, AAC</li>
|
||||||
|
<li><a href="api/stream.m3u8?src=${src}&mp4=flac">stream.m3u8</a> modern HLS/fMP4 / browsers: Safari all, Chrome Android / codecs: H264, H265*, AAC, FLAC (PCMA, PCMU, PCM)</li>
|
||||||
|
|
||||||
<h2>MJPEG source</h2>
|
<h2>MJPEG source</h2>
|
||||||
<li><a href="stream.html?src=${src}&mode=mjpeg">stream.html</a> with MJPEG mode / browsers: all / codecs: MJPEG, JPEG</li>
|
<li><a href="stream.html?src=${src}&mode=mjpeg">stream.html</a> with MJPEG mode / browsers: all / codecs: MJPEG, JPEG</li>
|
||||||
|
|||||||
+2
-1
@@ -27,7 +27,8 @@ export class VideoRTC extends HTMLElement {
|
|||||||
"hvc1.1.6.L153.B0", // H.265 main 5.1 (Chromecast Ultra)
|
"hvc1.1.6.L153.B0", // H.265 main 5.1 (Chromecast Ultra)
|
||||||
"mp4a.40.2", // AAC LC
|
"mp4a.40.2", // AAC LC
|
||||||
"mp4a.40.5", // AAC HE
|
"mp4a.40.5", // AAC HE
|
||||||
"opus", // OPUS Chrome
|
"flac", // FLAC (PCM compatible)
|
||||||
|
"opus", // OPUS Chrome, Firefox
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user