mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2025-10-04 16:02:43 +08:00
Compare commits
24 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7b3505f4f4 | ||
![]() |
98af8c3dbf | ||
![]() |
762edf157a | ||
![]() |
4a633cd9b5 | ||
![]() |
f4d2c801f0 | ||
![]() |
fb4b609914 | ||
![]() |
56633229ed | ||
![]() |
2d49cfd4b6 | ||
![]() |
0f934be9b6 | ||
![]() |
c1d6adc189 | ||
![]() |
500b8720d5 | ||
![]() |
bef8e6454d | ||
![]() |
5243aca8e9 | ||
![]() |
69dd4d26ec | ||
![]() |
e93d89ec96 | ||
![]() |
ec56227900 | ||
![]() |
decd3af941 | ||
![]() |
e8e43f9d68 | ||
![]() |
a1fec1c6f6 | ||
![]() |
073acdfec9 | ||
![]() |
d05ab79f88 | ||
![]() |
e295bc4eaf | ||
![]() |
2f436bba4e | ||
![]() |
0e28b0c797 |
@@ -14,7 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "1.0.1"
|
var Version = "1.1.0"
|
||||||
var UserAgent = "go2rtc/" + Version
|
var UserAgent = "go2rtc/" + Version
|
||||||
|
|
||||||
var ConfigPath string
|
var ConfigPath string
|
||||||
|
@@ -15,11 +15,11 @@ func deviceInputSuffix(videoIdx, audioIdx int) string {
|
|||||||
audio := findMedia(streamer.KindAudio, audioIdx)
|
audio := findMedia(streamer.KindAudio, audioIdx)
|
||||||
switch {
|
switch {
|
||||||
case video != nil && audio != nil:
|
case video != nil && audio != nil:
|
||||||
return `"` + video.Title + `:` + audio.Title + `"`
|
return `"` + video.MID + `:` + audio.MID + `"`
|
||||||
case video != nil:
|
case video != nil:
|
||||||
return `"` + video.Title + `"`
|
return `"` + video.MID + `"`
|
||||||
case audio != nil:
|
case audio != nil:
|
||||||
return `"` + audio.Title + `"`
|
return `"` + audio.MID + `"`
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,5 @@ process:
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadMedia(kind, name string) *streamer.Media {
|
func loadMedia(kind, name string) *streamer.Media {
|
||||||
return &streamer.Media{
|
return &streamer.Media{Kind: kind, MID: name}
|
||||||
Kind: kind, Title: name,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -13,7 +13,7 @@ const deviceInputPrefix = "-f v4l2"
|
|||||||
|
|
||||||
func deviceInputSuffix(videoIdx, audioIdx int) string {
|
func deviceInputSuffix(videoIdx, audioIdx int) string {
|
||||||
video := findMedia(streamer.KindVideo, videoIdx)
|
video := findMedia(streamer.KindVideo, videoIdx)
|
||||||
return video.Title
|
return video.MID
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadMedias() {
|
func loadMedias() {
|
||||||
@@ -44,7 +44,5 @@ func loadMedia(kind, name string) *streamer.Media {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &streamer.Media{
|
return &streamer.Media{Kind: kind, MID: name}
|
||||||
Kind: kind, Title: name,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -15,11 +15,11 @@ func deviceInputSuffix(videoIdx, audioIdx int) string {
|
|||||||
audio := findMedia(streamer.KindAudio, audioIdx)
|
audio := findMedia(streamer.KindAudio, audioIdx)
|
||||||
switch {
|
switch {
|
||||||
case video != nil && audio != nil:
|
case video != nil && audio != nil:
|
||||||
return `video="` + video.Title + `":audio=` + audio.Title + `"`
|
return `video="` + video.MID + `":audio=` + audio.MID + `"`
|
||||||
case video != nil:
|
case video != nil:
|
||||||
return `video="` + video.Title + `"`
|
return `video="` + video.MID + `"`
|
||||||
case audio != nil:
|
case audio != nil:
|
||||||
return `audio="` + audio.Title + `"`
|
return `audio="` + audio.MID + `"`
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,5 @@ func loadMedias() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadMedia(kind, name string) *streamer.Media {
|
func loadMedia(kind, name string) *streamer.Media {
|
||||||
return &streamer.Media{
|
return &streamer.Media{Kind: kind, MID: name}
|
||||||
Kind: kind, Title: name,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -67,6 +67,7 @@ var defaults = map[string]string{
|
|||||||
"pcma/48000": "-c:a pcm_alaw -ar:a 48000 -ac:a 1",
|
"pcma/48000": "-c:a pcm_alaw -ar:a 48000 -ac:a 1",
|
||||||
"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",
|
||||||
|
|
||||||
// hardware Intel and AMD on Linux
|
// hardware Intel and AMD on Linux
|
||||||
// better not to set `-async_depth:v 1` like for QSV, because framedrops
|
// better not to set `-async_depth:v 1` like for QSV, because framedrops
|
||||||
@@ -141,6 +142,8 @@ func parseArgs(s string) *Args {
|
|||||||
s += "?video"
|
s += "?video"
|
||||||
case args.audio > 0 && args.video == 0:
|
case args.audio > 0 && args.video == 0:
|
||||||
s += "?audio"
|
s += "?audio"
|
||||||
|
default:
|
||||||
|
s += "?video&audio"
|
||||||
}
|
}
|
||||||
args.input = strings.Replace(defaults["rtsp"], "{input}", s, 1)
|
args.input = strings.Replace(defaults["rtsp"], "{input}", s, 1)
|
||||||
} else if strings.HasPrefix(s, "device?") {
|
} else if strings.HasPrefix(s, "device?") {
|
||||||
|
261
cmd/hls/hls.go
Normal file
261
cmd/hls/hls.go
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
package hls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/ts"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
api.HandleFunc("api/stream.m3u8", handlerStream)
|
||||||
|
api.HandleFunc("api/hls/playlist.m3u8", handlerPlaylist)
|
||||||
|
|
||||||
|
// HLS (TS)
|
||||||
|
api.HandleFunc("api/hls/segment.ts", handlerSegmentTS)
|
||||||
|
|
||||||
|
// HLS (fMP4)
|
||||||
|
api.HandleFunc("api/hls/init.mp4", handlerInit)
|
||||||
|
api.HandleFunc("api/hls/segment.m4s", handlerSegmentMP4)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Consumer interface {
|
||||||
|
streamer.Consumer
|
||||||
|
Init() ([]byte, error)
|
||||||
|
MimeCodecs() string
|
||||||
|
Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
cons Consumer
|
||||||
|
playlist string
|
||||||
|
init []byte
|
||||||
|
segment []byte
|
||||||
|
seq int
|
||||||
|
alive *time.Timer
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
const keepalive = 5 * time.Second
|
||||||
|
|
||||||
|
var sessions = map[string]*Session{}
|
||||||
|
|
||||||
|
func handlerStream(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// CORS important for Chromecast
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Content-Type", "application/vnd.apple.mpegurl")
|
||||||
|
|
||||||
|
if r.Method == "OPTIONS" {
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
src := r.URL.Query().Get("src")
|
||||||
|
stream := streams.GetOrNew(src)
|
||||||
|
if stream == nil {
|
||||||
|
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var cons Consumer
|
||||||
|
|
||||||
|
// use fMP4 with codecs filter and TS without
|
||||||
|
medias := mp4.ParseQuery(r.URL.Query())
|
||||||
|
if medias != nil {
|
||||||
|
cons = &mp4.Consumer{
|
||||||
|
RemoteAddr: r.RemoteAddr,
|
||||||
|
UserAgent: r.UserAgent(),
|
||||||
|
Medias: medias,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cons = &ts.Consumer{
|
||||||
|
RemoteAddr: r.RemoteAddr,
|
||||||
|
UserAgent: r.UserAgent(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
session := &Session{cons: cons}
|
||||||
|
|
||||||
|
cons.Listen(func(msg interface{}) {
|
||||||
|
if data, ok := msg.([]byte); ok {
|
||||||
|
session.mu.Lock()
|
||||||
|
session.segment = append(session.segment, data...)
|
||||||
|
session.mu.Unlock()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session.alive = time.AfterFunc(keepalive, func() {
|
||||||
|
stream.RemoveConsumer(cons)
|
||||||
|
})
|
||||||
|
session.init, _ = cons.Init()
|
||||||
|
|
||||||
|
cons.Start()
|
||||||
|
|
||||||
|
sid := strconv.FormatInt(time.Now().UnixNano(), 10)
|
||||||
|
|
||||||
|
// two segments important for Chromecast
|
||||||
|
if medias != nil {
|
||||||
|
session.playlist = `#EXTM3U
|
||||||
|
#EXT-X-VERSION:6
|
||||||
|
#EXT-X-TARGETDURATION:1
|
||||||
|
#EXT-X-MEDIA-SEQUENCE:%d
|
||||||
|
#EXT-X-MAP:URI="init.mp4?id=` + sid + `"
|
||||||
|
#EXTINF:0.500,
|
||||||
|
segment.m4s?id=` + sid + `&n=%d
|
||||||
|
#EXTINF:0.500,
|
||||||
|
segment.m4s?id=` + sid + `&n=%d`
|
||||||
|
} else {
|
||||||
|
session.playlist = `#EXTM3U
|
||||||
|
#EXT-X-VERSION:3
|
||||||
|
#EXT-X-TARGETDURATION:1
|
||||||
|
#EXT-X-MEDIA-SEQUENCE:%d
|
||||||
|
#EXTINF:0.500,
|
||||||
|
segment.ts?id=` + sid + `&n=%d
|
||||||
|
#EXTINF:0.500,
|
||||||
|
segment.ts?id=` + sid + `&n=%d`
|
||||||
|
}
|
||||||
|
|
||||||
|
sessions[sid] = session
|
||||||
|
|
||||||
|
// bandwidth important for Safari, codecs useful for smooth playback
|
||||||
|
data := []byte(`#EXTM3U
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS="` + cons.MimeCodecs() + `"
|
||||||
|
hls/playlist.m3u8?id=` + sid)
|
||||||
|
|
||||||
|
if _, err := w.Write(data); err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerPlaylist(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Content-Type", "application/vnd.apple.mpegurl")
|
||||||
|
|
||||||
|
if r.Method == "OPTIONS" {
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sid := r.URL.Query().Get("id")
|
||||||
|
session := sessions[sid]
|
||||||
|
if session == nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s := fmt.Sprintf(session.playlist, session.seq, session.seq, session.seq+1)
|
||||||
|
|
||||||
|
if _, err := w.Write([]byte(s)); err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerSegmentTS(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Content-Type", "video/mp2t")
|
||||||
|
|
||||||
|
if r.Method == "OPTIONS" {
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sid := r.URL.Query().Get("id")
|
||||||
|
session := sessions[sid]
|
||||||
|
if session == nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session.alive.Reset(keepalive)
|
||||||
|
|
||||||
|
var i byte
|
||||||
|
for len(session.segment) == 0 {
|
||||||
|
if i++; i > 10 {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(time.Millisecond * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
session.mu.Lock()
|
||||||
|
data := session.segment
|
||||||
|
// important to start new segment with init
|
||||||
|
session.segment = session.init
|
||||||
|
session.seq++
|
||||||
|
session.mu.Unlock()
|
||||||
|
|
||||||
|
if _, err := w.Write(data); err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerInit(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Add("Content-Type", "video/mp4")
|
||||||
|
|
||||||
|
if r.Method == "OPTIONS" {
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sid := r.URL.Query().Get("id")
|
||||||
|
session := sessions[sid]
|
||||||
|
if session == nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := w.Write(session.init); err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerSegmentMP4(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Add("Content-Type", "video/iso.segment")
|
||||||
|
|
||||||
|
if r.Method == "OPTIONS" {
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sid := r.URL.Query().Get("id")
|
||||||
|
session := sessions[sid]
|
||||||
|
if session == nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session.alive.Reset(keepalive)
|
||||||
|
|
||||||
|
var i byte
|
||||||
|
for len(session.segment) == 0 {
|
||||||
|
if i++; i > 10 {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(time.Millisecond * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
session.mu.Lock()
|
||||||
|
data := session.segment
|
||||||
|
session.segment = nil
|
||||||
|
session.seq++
|
||||||
|
session.mu.Unlock()
|
||||||
|
|
||||||
|
if _, err := w.Write(data); err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
}
|
||||||
|
}
|
@@ -5,6 +5,7 @@ import (
|
|||||||
"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/mp4"
|
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -25,8 +26,14 @@ func Init() {
|
|||||||
var log zerolog.Logger
|
var log zerolog.Logger
|
||||||
|
|
||||||
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
||||||
if isChromeFirst(w, r) {
|
// Chrome 105 does two requests: without Range and with `Range: bytes=0-`
|
||||||
return
|
ua := r.UserAgent()
|
||||||
|
if strings.Contains(ua, " Chrome/") {
|
||||||
|
if r.Header.Values("Range") == nil {
|
||||||
|
w.Header().Set("Content-Type", "video/mp4")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
src := r.URL.Query().Get("src")
|
src := r.URL.Query().Get("src")
|
||||||
@@ -67,7 +74,22 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
|||||||
func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Trace().Msgf("[mp4] %s %+v", r.Method, r.Header)
|
log.Trace().Msgf("[mp4] %s %+v", r.Method, r.Header)
|
||||||
|
|
||||||
if isChromeFirst(w, r) || isSafari(w, r) {
|
// Chrome has Safari in UA, so check first Chrome and later Safari
|
||||||
|
ua := r.UserAgent()
|
||||||
|
if strings.Contains(ua, " Chrome/") {
|
||||||
|
if r.Header.Values("Range") == nil {
|
||||||
|
w.Header().Set("Content-Type", "video/mp4")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if strings.Contains(ua, " Safari/") {
|
||||||
|
// auto redirect to HLS/fMP4 format, because Safari not support MP4 stream
|
||||||
|
url := "stream.m3u8?" + r.URL.RawQuery
|
||||||
|
if !r.URL.Query().Has("mp4") {
|
||||||
|
url += "&mp4"
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, url, http.StatusMovedPermanently)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +105,9 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
|||||||
cons := &mp4.Consumer{
|
cons := &mp4.Consumer{
|
||||||
RemoteAddr: r.RemoteAddr,
|
RemoteAddr: r.RemoteAddr,
|
||||||
UserAgent: r.UserAgent(),
|
UserAgent: r.UserAgent(),
|
||||||
|
Medias: streamer.ParseQuery(r.URL.Query()),
|
||||||
}
|
}
|
||||||
|
|
||||||
cons.Listen(func(msg interface{}) {
|
cons.Listen(func(msg interface{}) {
|
||||||
if data, ok := msg.([]byte); ok {
|
if data, ok := msg.([]byte); ok {
|
||||||
if _, err := w.Write(data); err != nil && exit != nil {
|
if _, err := w.Write(data); err != nil && exit != nil {
|
||||||
@@ -135,23 +159,3 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
|||||||
duration.Stop()
|
duration.Stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isChromeFirst(w http.ResponseWriter, r *http.Request) bool {
|
|
||||||
// Chrome 105 does two requests: without Range and with `Range: bytes=0-`
|
|
||||||
if strings.Contains(r.UserAgent(), " Chrome/") {
|
|
||||||
if r.Header.Values("Range") == nil {
|
|
||||||
w.Header().Set("Content-Type", "video/mp4")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isSafari(w http.ResponseWriter, r *http.Request) bool {
|
|
||||||
if r.Header.Get("Range") == "bytes=0-1" {
|
|
||||||
handlerKeyframe(w, r)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
@@ -22,8 +22,6 @@ func handlerWSMSE(tr *api.Transport, msg *api.Message) error {
|
|||||||
RemoteAddr: tr.Request.RemoteAddr,
|
RemoteAddr: tr.Request.RemoteAddr,
|
||||||
UserAgent: tr.Request.UserAgent(),
|
UserAgent: tr.Request.UserAgent(),
|
||||||
}
|
}
|
||||||
cons.UserAgent = tr.Request.UserAgent()
|
|
||||||
cons.RemoteAddr = tr.Request.RemoteAddr
|
|
||||||
|
|
||||||
if codecs, ok := msg.Value.(string); ok {
|
if codecs, ok := msg.Value.(string); ok {
|
||||||
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MSE consumer")
|
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MSE consumer")
|
||||||
@@ -108,15 +106,18 @@ func parseMedias(codecs string, parseAudio bool) (medias []*streamer.Media) {
|
|||||||
|
|
||||||
for _, name := range strings.Split(codecs, ",") {
|
for _, name := range strings.Split(codecs, ",") {
|
||||||
switch name {
|
switch name {
|
||||||
case "avc1.640029":
|
case mp4.MimeH264:
|
||||||
codec := &streamer.Codec{Name: streamer.CodecH264}
|
codec := &streamer.Codec{Name: streamer.CodecH264}
|
||||||
videos = append(videos, codec)
|
videos = append(videos, codec)
|
||||||
case "hvc1.1.6.L153.B0":
|
case mp4.MimeH265:
|
||||||
codec := &streamer.Codec{Name: streamer.CodecH265}
|
codec := &streamer.Codec{Name: streamer.CodecH265}
|
||||||
videos = append(videos, codec)
|
videos = append(videos, codec)
|
||||||
case "mp4a.40.2":
|
case mp4.MimeAAC:
|
||||||
codec := &streamer.Codec{Name: streamer.CodecAAC}
|
codec := &streamer.Codec{Name: streamer.CodecAAC}
|
||||||
audios = append(audios, codec)
|
audios = append(audios, codec)
|
||||||
|
case mp4.MimeOpus:
|
||||||
|
codec := &streamer.Codec{Name: streamer.CodecOpus}
|
||||||
|
audios = append(audios, codec)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,25 +3,29 @@ package rtsp
|
|||||||
import (
|
import (
|
||||||
"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/mp4"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/rtsp"
|
"github.com/AlexxIT/go2rtc/pkg/rtsp"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"net"
|
"net"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
var conf struct {
|
var conf struct {
|
||||||
Mod struct {
|
Mod struct {
|
||||||
Listen string `yaml:"listen" json:"listen"`
|
Listen string `yaml:"listen" json:"listen"`
|
||||||
Username string `yaml:"username" json:"-"`
|
Username string `yaml:"username" json:"-"`
|
||||||
Password string `yaml:"password" json:"-"`
|
Password string `yaml:"password" json:"-"`
|
||||||
|
DefaultQuery string `yaml:"default_query"`
|
||||||
} `yaml:"rtsp"`
|
} `yaml:"rtsp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// default config
|
// default config
|
||||||
conf.Mod.Listen = ":8554"
|
conf.Mod.Listen = ":8554"
|
||||||
|
conf.Mod.DefaultQuery = "video&audio"
|
||||||
|
|
||||||
app.LoadConfig(&conf)
|
app.LoadConfig(&conf)
|
||||||
app.Info["rtsp"] = conf.Mod
|
app.Info["rtsp"] = conf.Mod
|
||||||
@@ -49,6 +53,10 @@ func Init() {
|
|||||||
|
|
||||||
log.Info().Str("addr", address).Msg("[rtsp] listen")
|
log.Info().Str("addr", address).Msg("[rtsp] listen")
|
||||||
|
|
||||||
|
if query, err := url.ParseQuery(conf.Mod.DefaultQuery); err == nil {
|
||||||
|
defaultMedias = mp4.ParseQuery(query)
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
conn, err := ln.Accept()
|
conn, err := ln.Accept()
|
||||||
@@ -78,6 +86,7 @@ var Port string
|
|||||||
|
|
||||||
var log zerolog.Logger
|
var log zerolog.Logger
|
||||||
var handlers []Handler
|
var handlers []Handler
|
||||||
|
var defaultMedias []*streamer.Media
|
||||||
|
|
||||||
func rtspHandler(url string) (streamer.Producer, error) {
|
func rtspHandler(url string) (streamer.Producer, error) {
|
||||||
backchannel := true
|
backchannel := true
|
||||||
@@ -164,7 +173,10 @@ func tcpHandler(conn *rtsp.Conn) {
|
|||||||
|
|
||||||
conn.SessionName = app.UserAgent
|
conn.SessionName = app.UserAgent
|
||||||
|
|
||||||
initMedias(conn)
|
conn.Medias = mp4.ParseQuery(conn.URL.Query())
|
||||||
|
if conn.Medias == nil {
|
||||||
|
conn.Medias = defaultMedias
|
||||||
|
}
|
||||||
|
|
||||||
if err := stream.AddConsumer(conn); err != nil {
|
if err := stream.AddConsumer(conn); err != nil {
|
||||||
log.Warn().Err(err).Str("stream", name).Msg("[rtsp]")
|
log.Warn().Err(err).Str("stream", name).Msg("[rtsp]")
|
||||||
@@ -228,45 +240,3 @@ func tcpHandler(conn *rtsp.Conn) {
|
|||||||
|
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func initMedias(conn *rtsp.Conn) {
|
|
||||||
// set media candidates from query list
|
|
||||||
for key, value := range conn.URL.Query() {
|
|
||||||
switch key {
|
|
||||||
case streamer.KindVideo, streamer.KindAudio:
|
|
||||||
for _, name := range value {
|
|
||||||
name = strings.ToUpper(name)
|
|
||||||
|
|
||||||
// check aliases
|
|
||||||
switch name {
|
|
||||||
case "COPY":
|
|
||||||
name = "" // pass empty codecs list
|
|
||||||
case "MJPEG":
|
|
||||||
name = streamer.CodecJPEG
|
|
||||||
case "AAC":
|
|
||||||
name = streamer.CodecAAC
|
|
||||||
}
|
|
||||||
|
|
||||||
media := &streamer.Media{
|
|
||||||
Kind: key, Direction: streamer.DirectionRecvonly,
|
|
||||||
}
|
|
||||||
|
|
||||||
// empty codecs match all codecs
|
|
||||||
if name != "" {
|
|
||||||
// empty clock rate and channels match any values
|
|
||||||
media.Codecs = []*streamer.Codec{{Name: name}}
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.Medias = append(conn.Medias, media)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// set default media candidates if query is empty
|
|
||||||
if conn.Medias == nil {
|
|
||||||
conn.Medias = []*streamer.Media{
|
|
||||||
{Kind: streamer.KindVideo, Direction: streamer.DirectionRecvonly},
|
|
||||||
{Kind: streamer.KindAudio, Direction: streamer.DirectionRecvonly},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -91,7 +91,9 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
|
|||||||
|
|
||||||
consumer.tracks = append(consumer.tracks, consTrack)
|
consumer.tracks = append(consumer.tracks, consTrack)
|
||||||
producers = append(producers, prod)
|
producers = append(producers, prod)
|
||||||
break producers
|
if !consMedia.MatchAll() {
|
||||||
|
break producers
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
19
main.go
19
main.go
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/cmd/exec"
|
"github.com/AlexxIT/go2rtc/cmd/exec"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/ffmpeg"
|
"github.com/AlexxIT/go2rtc/cmd/ffmpeg"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/hass"
|
"github.com/AlexxIT/go2rtc/cmd/hass"
|
||||||
|
"github.com/AlexxIT/go2rtc/cmd/hls"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/homekit"
|
"github.com/AlexxIT/go2rtc/cmd/homekit"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/http"
|
"github.com/AlexxIT/go2rtc/cmd/http"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/ivideon"
|
"github.com/AlexxIT/go2rtc/cmd/ivideon"
|
||||||
@@ -26,27 +27,25 @@ import (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app.Init() // init config and logs
|
app.Init() // init config and logs
|
||||||
|
api.Init() // init HTTP API server
|
||||||
streams.Init() // load streams list
|
streams.Init() // load streams list
|
||||||
|
|
||||||
api.Init() // init HTTP API server
|
|
||||||
|
|
||||||
echo.Init()
|
|
||||||
|
|
||||||
rtsp.Init() // add support RTSP client and RTSP server
|
rtsp.Init() // add support RTSP client and RTSP server
|
||||||
rtmp.Init() // add support RTMP client
|
rtmp.Init() // add support RTMP client
|
||||||
exec.Init() // add support exec scheme (depends on RTSP server)
|
exec.Init() // add support exec scheme (depends on RTSP server)
|
||||||
ffmpeg.Init() // add support ffmpeg scheme (depends on exec scheme)
|
ffmpeg.Init() // add support ffmpeg scheme (depends on exec scheme)
|
||||||
hass.Init() // add support hass scheme
|
hass.Init() // add support hass scheme
|
||||||
|
echo.Init()
|
||||||
webrtc.Init()
|
ivideon.Init()
|
||||||
mp4.Init()
|
|
||||||
mjpeg.Init()
|
|
||||||
http.Init()
|
|
||||||
|
|
||||||
srtp.Init()
|
srtp.Init()
|
||||||
homekit.Init()
|
homekit.Init()
|
||||||
|
|
||||||
ivideon.Init()
|
webrtc.Init()
|
||||||
|
mp4.Init()
|
||||||
|
hls.Init()
|
||||||
|
mjpeg.Init()
|
||||||
|
http.Init()
|
||||||
|
|
||||||
ngrok.Init()
|
ngrok.Init()
|
||||||
debug.Init()
|
debug.Init()
|
||||||
|
@@ -17,9 +17,14 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
|||||||
|
|
||||||
//log.Printf("[RTP/AAC] units: %d, size: %4d, ts: %10d, %t", headersSize/2, len(packet.Payload), packet.Timestamp, packet.Marker)
|
//log.Printf("[RTP/AAC] units: %d, size: %4d, ts: %10d, %t", headersSize/2, len(packet.Payload), packet.Timestamp, packet.Marker)
|
||||||
|
|
||||||
|
data := packet.Payload[2+headersSize:]
|
||||||
|
if IsADTS(data) {
|
||||||
|
data = data[7:]
|
||||||
|
}
|
||||||
|
|
||||||
clone := *packet
|
clone := *packet
|
||||||
clone.Version = RTPPacketVersionAAC
|
clone.Version = RTPPacketVersionAAC
|
||||||
clone.Payload = packet.Payload[2+headersSize:]
|
clone.Payload = data
|
||||||
return push(&clone)
|
return push(&clone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,3 +60,7 @@ func RTPPay(mtu uint16) streamer.WrapperFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsADTS(b []byte) bool {
|
||||||
|
return len(b) > 7 && b[0] == 0xFF && b[1]&0xF0 == 0xF0
|
||||||
|
}
|
||||||
|
318
pkg/iso/atoms.go
Normal file
318
pkg/iso/atoms.go
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
package iso
|
||||||
|
|
||||||
|
const (
|
||||||
|
Ftyp = "ftyp"
|
||||||
|
Moov = "moov"
|
||||||
|
MoovMvhd = "mvhd"
|
||||||
|
MoovTrak = "trak"
|
||||||
|
MoovTrakTkhd = "tkhd"
|
||||||
|
MoovTrakMdia = "mdia"
|
||||||
|
MoovTrakMdiaMdhd = "mdhd"
|
||||||
|
MoovTrakMdiaHdlr = "hdlr"
|
||||||
|
MoovTrakMdiaMinf = "minf"
|
||||||
|
MoovTrakMdiaMinfVmhd = "vmhd"
|
||||||
|
MoovTrakMdiaMinfSmhd = "smhd"
|
||||||
|
MoovTrakMdiaMinfDinf = "dinf"
|
||||||
|
MoovTrakMdiaMinfDinfDref = "dref"
|
||||||
|
MoovTrakMdiaMinfDinfDrefUrl = "url "
|
||||||
|
MoovTrakMdiaMinfStbl = "stbl"
|
||||||
|
MoovTrakMdiaMinfStblStsd = "stsd"
|
||||||
|
MoovTrakMdiaMinfStblStts = "stts"
|
||||||
|
MoovTrakMdiaMinfStblStsc = "stsc"
|
||||||
|
MoovTrakMdiaMinfStblStsz = "stsz"
|
||||||
|
MoovTrakMdiaMinfStblStco = "stco"
|
||||||
|
MoovMvex = "mvex"
|
||||||
|
MoovMvexTrex = "trex"
|
||||||
|
Moof = "moof"
|
||||||
|
MoofMfhd = "mfhd"
|
||||||
|
MoofTraf = "traf"
|
||||||
|
MoofTrafTfhd = "tfhd"
|
||||||
|
MoofTrafTfdt = "tfdt"
|
||||||
|
MoofTrafTrun = "trun"
|
||||||
|
Mdat = "mdat"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Movie) WriteFileType() {
|
||||||
|
m.StartAtom(Ftyp)
|
||||||
|
m.WriteString("iso5")
|
||||||
|
m.WriteUint32(512)
|
||||||
|
m.WriteString("iso5")
|
||||||
|
m.WriteString("iso6")
|
||||||
|
m.WriteString("mp41")
|
||||||
|
m.EndAtom()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) WriteMovieHeader() {
|
||||||
|
m.StartAtom(MoovMvhd)
|
||||||
|
m.Skip(1) // version
|
||||||
|
m.Skip(3) // flags
|
||||||
|
m.Skip(4) // create time
|
||||||
|
m.Skip(4) // modify time
|
||||||
|
m.WriteUint32(1000) // time scale
|
||||||
|
m.Skip(4) // duration
|
||||||
|
m.WriteFloat32(1) // preferred rate
|
||||||
|
m.WriteFloat16(1) // preferred volume
|
||||||
|
m.Skip(10) // reserved
|
||||||
|
m.WriteMatrix()
|
||||||
|
m.Skip(6 * 4) // predefined?
|
||||||
|
m.WriteUint32(0xFFFFFFFF) // next track ID
|
||||||
|
m.EndAtom()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) WriteTrackHeader(id uint32, width, height uint16) {
|
||||||
|
const (
|
||||||
|
TkhdTrackEnabled = 0x0001
|
||||||
|
TkhdTrackInMovie = 0x0002
|
||||||
|
TkhdTrackInPreview = 0x0004
|
||||||
|
TkhdTrackInPoster = 0x0008
|
||||||
|
)
|
||||||
|
|
||||||
|
// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-32963
|
||||||
|
m.StartAtom(MoovTrakTkhd)
|
||||||
|
m.Skip(1) // version
|
||||||
|
m.WriteUint24(TkhdTrackEnabled | TkhdTrackInMovie)
|
||||||
|
m.Skip(4) // create time
|
||||||
|
m.Skip(4) // modify time
|
||||||
|
m.WriteUint32(id) // trackID
|
||||||
|
m.Skip(4) // reserved
|
||||||
|
m.Skip(4) // duration
|
||||||
|
m.Skip(8) // reserved
|
||||||
|
m.Skip(2) // layer
|
||||||
|
if width > 0 {
|
||||||
|
m.Skip(2)
|
||||||
|
m.Skip(2)
|
||||||
|
} else {
|
||||||
|
m.WriteUint16(1) // alternate group
|
||||||
|
m.WriteFloat16(1) // volume
|
||||||
|
}
|
||||||
|
m.Skip(2) // reserved
|
||||||
|
m.WriteMatrix()
|
||||||
|
if width > 0 {
|
||||||
|
m.WriteFloat32(float64(width))
|
||||||
|
m.WriteFloat32(float64(height))
|
||||||
|
} else {
|
||||||
|
m.Skip(4)
|
||||||
|
m.Skip(4)
|
||||||
|
}
|
||||||
|
m.EndAtom()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) WriteMediaHeader(timescale uint32) {
|
||||||
|
// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-32999
|
||||||
|
m.StartAtom(MoovTrakMdiaMdhd)
|
||||||
|
m.Skip(1) // version
|
||||||
|
m.Skip(3) // flags
|
||||||
|
m.Skip(4) // creation time
|
||||||
|
m.Skip(4) // modification time
|
||||||
|
m.WriteUint32(timescale) // timescale
|
||||||
|
m.Skip(4) // duration
|
||||||
|
m.WriteUint16(0x55C4) // language (Unspecified)
|
||||||
|
m.Skip(2) // quality
|
||||||
|
m.EndAtom()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) WriteMediaHandler(s, name string) {
|
||||||
|
// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-33004
|
||||||
|
m.StartAtom(MoovTrakMdiaHdlr)
|
||||||
|
m.Skip(1) // version
|
||||||
|
m.Skip(3) // flags
|
||||||
|
m.Skip(4)
|
||||||
|
m.WriteString(s) // handler type (4 byte!)
|
||||||
|
m.Skip(3 * 4) // reserved
|
||||||
|
m.WriteString(name) // handler name (any len)
|
||||||
|
m.Skip(1) // end string
|
||||||
|
m.EndAtom()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) WriteVideoMediaInfo() {
|
||||||
|
// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-33012
|
||||||
|
m.StartAtom(MoovTrakMdiaMinfVmhd)
|
||||||
|
m.Skip(1) // version
|
||||||
|
m.WriteUint24(1) // flags (You should always set this flag to 1)
|
||||||
|
m.Skip(2) // graphics mode
|
||||||
|
m.Skip(3 * 2) // op color
|
||||||
|
m.EndAtom()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) WriteAudioMediaInfo() {
|
||||||
|
m.StartAtom(MoovTrakMdiaMinfSmhd)
|
||||||
|
m.Skip(1) // version
|
||||||
|
m.Skip(3) // flags
|
||||||
|
m.Skip(4) // balance
|
||||||
|
m.EndAtom()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) WriteDataInfo() {
|
||||||
|
// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25680
|
||||||
|
m.StartAtom(MoovTrakMdiaMinfDinf)
|
||||||
|
m.StartAtom(MoovTrakMdiaMinfDinfDref)
|
||||||
|
m.Skip(1) // version
|
||||||
|
m.Skip(3) // flags
|
||||||
|
m.WriteUint32(1) // childrens
|
||||||
|
|
||||||
|
m.StartAtom(MoovTrakMdiaMinfDinfDrefUrl)
|
||||||
|
m.Skip(1) // version
|
||||||
|
m.WriteUint24(1) // flags (self reference)
|
||||||
|
m.EndAtom()
|
||||||
|
|
||||||
|
m.EndAtom() // DREF
|
||||||
|
m.EndAtom() // DINF
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) WriteSampleTable(writeSampleDesc func()) {
|
||||||
|
// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-33040
|
||||||
|
m.StartAtom(MoovTrakMdiaMinfStbl)
|
||||||
|
|
||||||
|
m.StartAtom(MoovTrakMdiaMinfStblStsd)
|
||||||
|
m.Skip(1) // version
|
||||||
|
m.Skip(3) // flags
|
||||||
|
m.WriteUint32(1) // entry count
|
||||||
|
writeSampleDesc()
|
||||||
|
m.EndAtom()
|
||||||
|
|
||||||
|
m.StartAtom(MoovTrakMdiaMinfStblStts)
|
||||||
|
m.Skip(1) // version
|
||||||
|
m.Skip(3) // flags
|
||||||
|
m.Skip(4) // entry count
|
||||||
|
m.EndAtom()
|
||||||
|
|
||||||
|
m.StartAtom(MoovTrakMdiaMinfStblStsc)
|
||||||
|
m.Skip(1) // version
|
||||||
|
m.Skip(3) // flags
|
||||||
|
m.Skip(4) // entry count
|
||||||
|
m.EndAtom()
|
||||||
|
|
||||||
|
m.StartAtom(MoovTrakMdiaMinfStblStsz)
|
||||||
|
m.Skip(1) // version
|
||||||
|
m.Skip(3) // flags
|
||||||
|
m.Skip(4) // sample size
|
||||||
|
m.Skip(4) // entry count
|
||||||
|
m.EndAtom()
|
||||||
|
|
||||||
|
m.StartAtom(MoovTrakMdiaMinfStblStco)
|
||||||
|
m.Skip(1) // version
|
||||||
|
m.Skip(3) // flags
|
||||||
|
m.Skip(4) // entry count
|
||||||
|
m.EndAtom()
|
||||||
|
|
||||||
|
m.EndAtom()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) WriteTrackExtend(id uint32) {
|
||||||
|
m.StartAtom(MoovMvexTrex)
|
||||||
|
m.Skip(1) // version
|
||||||
|
m.Skip(3) // flags
|
||||||
|
m.WriteUint32(id) // trackID
|
||||||
|
m.WriteUint32(1) // default sample description index
|
||||||
|
m.Skip(4) // default sample duration
|
||||||
|
m.Skip(4) // default sample size
|
||||||
|
m.Skip(4) // default sample flags
|
||||||
|
m.EndAtom()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) WriteVideoTrack(id uint32, codec string, timescale uint32, width, height uint16, conf []byte) {
|
||||||
|
m.StartAtom(MoovTrak)
|
||||||
|
m.WriteTrackHeader(id, width, height)
|
||||||
|
|
||||||
|
m.StartAtom(MoovTrakMdia)
|
||||||
|
m.WriteMediaHeader(timescale)
|
||||||
|
m.WriteMediaHandler("vide", "VideoHandler")
|
||||||
|
|
||||||
|
m.StartAtom(MoovTrakMdiaMinf)
|
||||||
|
m.WriteVideoMediaInfo()
|
||||||
|
m.WriteDataInfo()
|
||||||
|
m.WriteSampleTable(func() {
|
||||||
|
m.WriteVideo(codec, width, height, conf)
|
||||||
|
})
|
||||||
|
m.EndAtom() // MINF
|
||||||
|
|
||||||
|
m.EndAtom() // MDIA
|
||||||
|
m.EndAtom() // TRAK
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) WriteAudioTrack(id uint32, codec string, timescale uint32, channels uint16, conf []byte) {
|
||||||
|
m.StartAtom(MoovTrak)
|
||||||
|
m.WriteTrackHeader(id, 0, 0)
|
||||||
|
|
||||||
|
m.StartAtom(MoovTrakMdia)
|
||||||
|
m.WriteMediaHeader(timescale)
|
||||||
|
m.WriteMediaHandler("soun", "SoundHandler")
|
||||||
|
|
||||||
|
m.StartAtom(MoovTrakMdiaMinf)
|
||||||
|
m.WriteAudioMediaInfo()
|
||||||
|
m.WriteDataInfo()
|
||||||
|
m.WriteSampleTable(func() {
|
||||||
|
m.WriteAudio(codec, channels, timescale, conf)
|
||||||
|
})
|
||||||
|
m.EndAtom() // MINF
|
||||||
|
|
||||||
|
m.EndAtom() // MDIA
|
||||||
|
m.EndAtom() // TRAK
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) WriteMovieFragment(seq, tid, duration, size uint32, time uint64) {
|
||||||
|
m.StartAtom(Moof)
|
||||||
|
|
||||||
|
m.StartAtom(MoofMfhd)
|
||||||
|
m.Skip(1) // version
|
||||||
|
m.Skip(3) // flags
|
||||||
|
m.WriteUint32(seq) // sequence number
|
||||||
|
m.EndAtom()
|
||||||
|
|
||||||
|
m.StartAtom(MoofTraf)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TfhdDefaultSampleDuration = 0x000008
|
||||||
|
TfhdDefaultSampleSize = 0x000010
|
||||||
|
TfhdDefaultSampleFlags = 0x000020
|
||||||
|
TfhdDefaultBaseIsMoof = 0x020000
|
||||||
|
)
|
||||||
|
|
||||||
|
m.StartAtom(MoofTrafTfhd)
|
||||||
|
m.Skip(1) // version
|
||||||
|
m.WriteUint24(
|
||||||
|
TfhdDefaultSampleDuration |
|
||||||
|
TfhdDefaultSampleSize |
|
||||||
|
TfhdDefaultSampleFlags |
|
||||||
|
TfhdDefaultBaseIsMoof,
|
||||||
|
)
|
||||||
|
m.WriteUint32(tid) // track id
|
||||||
|
m.WriteUint32(duration) // default sample duration
|
||||||
|
m.WriteUint32(size) // default sample size
|
||||||
|
m.WriteUint32(0x2000000) // default sample flags
|
||||||
|
m.EndAtom()
|
||||||
|
|
||||||
|
m.StartAtom(MoofTrafTfdt)
|
||||||
|
m.WriteBytes(1) // version
|
||||||
|
m.Skip(3) // flags
|
||||||
|
m.WriteUint64(time) // base media decode time
|
||||||
|
m.EndAtom()
|
||||||
|
|
||||||
|
const (
|
||||||
|
TrunDataOffset = 0x000001
|
||||||
|
TrunFirstSampleFlags = 0x000004
|
||||||
|
TrunSampleDuration = 0x0000100
|
||||||
|
TrunSampleSize = 0x0000200
|
||||||
|
TrunSampleFlags = 0x0000400
|
||||||
|
TrunSampleCTS = 0x0000800
|
||||||
|
)
|
||||||
|
|
||||||
|
m.StartAtom(MoofTrafTrun)
|
||||||
|
m.Skip(1) // version
|
||||||
|
m.WriteUint24(TrunDataOffset) // flags
|
||||||
|
m.WriteUint32(1) // sample count
|
||||||
|
// data offset: current pos + uint32 len + MDAT header len
|
||||||
|
m.WriteUint32(uint32(len(m.b)) + 4 + 8)
|
||||||
|
m.EndAtom() // TRUN
|
||||||
|
|
||||||
|
m.EndAtom() // TRAF
|
||||||
|
|
||||||
|
m.EndAtom() // MOOF
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) WriteData(b []byte) {
|
||||||
|
m.StartAtom(Mdat)
|
||||||
|
m.Write(b)
|
||||||
|
m.EndAtom()
|
||||||
|
|
||||||
|
}
|
151
pkg/iso/codecs.go
Normal file
151
pkg/iso/codecs.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
package iso
|
||||||
|
|
||||||
|
import "github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
|
|
||||||
|
func (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) {
|
||||||
|
// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html
|
||||||
|
switch codec {
|
||||||
|
case streamer.CodecH264:
|
||||||
|
m.StartAtom("avc1")
|
||||||
|
case streamer.CodecH265:
|
||||||
|
m.StartAtom("hev1")
|
||||||
|
default:
|
||||||
|
panic("unsupported iso video: " + codec)
|
||||||
|
}
|
||||||
|
m.Skip(6)
|
||||||
|
m.WriteUint16(1) // data_reference_index
|
||||||
|
m.Skip(2) // version
|
||||||
|
m.Skip(2) // revision
|
||||||
|
m.Skip(4) // vendor
|
||||||
|
m.Skip(4) // temporal quality
|
||||||
|
m.Skip(4) // spatial quality
|
||||||
|
m.WriteUint16(width) // width
|
||||||
|
m.WriteUint16(height) // height
|
||||||
|
m.WriteFloat32(72) // horizontal resolution
|
||||||
|
m.WriteFloat32(72) // vertical resolution
|
||||||
|
m.Skip(4) // reserved
|
||||||
|
m.WriteUint16(1) // frame count
|
||||||
|
m.Skip(32) // compressor name
|
||||||
|
m.WriteUint16(24) // depth
|
||||||
|
m.WriteUint16(0xFFFF) // color table id (-1)
|
||||||
|
|
||||||
|
switch codec {
|
||||||
|
case streamer.CodecH264:
|
||||||
|
m.StartAtom("avcC")
|
||||||
|
case streamer.CodecH265:
|
||||||
|
m.StartAtom("hvcC")
|
||||||
|
}
|
||||||
|
m.Write(conf)
|
||||||
|
m.EndAtom() // AVCC
|
||||||
|
|
||||||
|
m.EndAtom() // AVC1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, conf []byte) {
|
||||||
|
switch codec {
|
||||||
|
case streamer.CodecAAC, streamer.CodecMP3:
|
||||||
|
m.StartAtom("mp4a")
|
||||||
|
case streamer.CodecOpus:
|
||||||
|
m.StartAtom("Opus")
|
||||||
|
case streamer.CodecPCMU:
|
||||||
|
m.StartAtom("ulaw")
|
||||||
|
case streamer.CodecPCMA:
|
||||||
|
m.StartAtom("alaw")
|
||||||
|
default:
|
||||||
|
panic("unsupported iso audio: " + codec)
|
||||||
|
}
|
||||||
|
m.Skip(6)
|
||||||
|
m.WriteUint16(1) // data_reference_index
|
||||||
|
m.Skip(2) // version
|
||||||
|
m.Skip(2) // revision
|
||||||
|
m.Skip(4) // vendor
|
||||||
|
m.WriteUint16(channels) // channel_count
|
||||||
|
m.WriteUint16(16) // sample_size
|
||||||
|
m.Skip(2) // compression id
|
||||||
|
m.Skip(2) // reserved
|
||||||
|
m.WriteFloat32(float64(sampleRate)) // sample_rate
|
||||||
|
|
||||||
|
switch codec {
|
||||||
|
case streamer.CodecAAC:
|
||||||
|
m.WriteEsdsAAC(conf)
|
||||||
|
case streamer.CodecMP3:
|
||||||
|
m.WriteEsdsMP3()
|
||||||
|
case streamer.CodecOpus:
|
||||||
|
// don't know what means this magic
|
||||||
|
m.StartAtom("dOps")
|
||||||
|
m.WriteBytes(0, 0x02, 0x01, 0x38, 0, 0, 0xBB, 0x80, 0, 0, 0)
|
||||||
|
m.EndAtom()
|
||||||
|
case streamer.CodecPCMU, streamer.CodecPCMA:
|
||||||
|
// don't know what means this magic
|
||||||
|
m.StartAtom("chan")
|
||||||
|
m.WriteBytes(0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0)
|
||||||
|
m.EndAtom()
|
||||||
|
}
|
||||||
|
|
||||||
|
m.EndAtom() // MP4A/OPUS
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) WriteEsdsAAC(conf []byte) {
|
||||||
|
m.StartAtom("esds")
|
||||||
|
m.Skip(1) // version
|
||||||
|
m.Skip(3) // flags
|
||||||
|
|
||||||
|
// MP4ESDescrTag[3]:
|
||||||
|
// - MP4DecConfigDescrTag[4]:
|
||||||
|
// - MP4DecSpecificDescrTag[5]: conf
|
||||||
|
// - Other[6]
|
||||||
|
const header = 5
|
||||||
|
const size3 = 3
|
||||||
|
const size4 = 13
|
||||||
|
size5 := byte(len(conf))
|
||||||
|
const size6 = 1
|
||||||
|
|
||||||
|
m.WriteBytes(3, 0x80, 0x80, 0x80, size3+header+size4+header+size5+header+size6)
|
||||||
|
m.Skip(2) // es id
|
||||||
|
m.Skip(1) // es flags
|
||||||
|
|
||||||
|
m.WriteBytes(4, 0x80, 0x80, 0x80, size4+header+size5)
|
||||||
|
m.WriteBytes(0x40) // object id
|
||||||
|
m.WriteBytes(0x15) // stream type
|
||||||
|
m.Skip(3) // buffer size db
|
||||||
|
m.Skip(4) // max bitraga
|
||||||
|
m.Skip(4) // avg bitraga
|
||||||
|
|
||||||
|
m.WriteBytes(5, 0x80, 0x80, 0x80, size5)
|
||||||
|
m.Write(conf)
|
||||||
|
|
||||||
|
m.WriteBytes(6, 0x80, 0x80, 0x80, 1)
|
||||||
|
m.WriteBytes(2) // ?
|
||||||
|
|
||||||
|
m.EndAtom() // ESDS
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) WriteEsdsMP3() {
|
||||||
|
m.StartAtom("esds")
|
||||||
|
m.Skip(1) // version
|
||||||
|
m.Skip(3) // flags
|
||||||
|
|
||||||
|
// MP4ESDescrTag[3]:
|
||||||
|
// - MP4DecConfigDescrTag[4]:
|
||||||
|
// - Other[6]
|
||||||
|
const header = 5
|
||||||
|
const size3 = 3
|
||||||
|
const size4 = 13
|
||||||
|
const size6 = 1
|
||||||
|
|
||||||
|
m.WriteBytes(3, 0x80, 0x80, 0x80, size3+header+size4+header+size6)
|
||||||
|
m.Skip(2) // es id
|
||||||
|
m.Skip(1) // es flags
|
||||||
|
|
||||||
|
m.WriteBytes(4, 0x80, 0x80, 0x80, size4)
|
||||||
|
m.WriteBytes(0x6B) // object id
|
||||||
|
m.WriteBytes(0x15) // stream type
|
||||||
|
m.Skip(3) // buffer size db
|
||||||
|
m.Skip(4) // max bitraga
|
||||||
|
m.Skip(4) // avg bitraga
|
||||||
|
|
||||||
|
m.WriteBytes(6, 0x80, 0x80, 0x80, 1)
|
||||||
|
m.WriteBytes(2) // ?
|
||||||
|
|
||||||
|
m.EndAtom() // ESDS
|
||||||
|
}
|
91
pkg/iso/iso.go
Normal file
91
pkg/iso/iso.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package iso
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Movie struct {
|
||||||
|
b []byte
|
||||||
|
start []int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMovie(size int) *Movie {
|
||||||
|
return &Movie{b: make([]byte, 0, size)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) Bytes() []byte {
|
||||||
|
return m.b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) StartAtom(name string) {
|
||||||
|
m.start = append(m.start, len(m.b))
|
||||||
|
m.b = append(m.b, 0, 0, 0, 0)
|
||||||
|
m.b = append(m.b, name...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) EndAtom() {
|
||||||
|
n := len(m.start) - 1
|
||||||
|
|
||||||
|
i := m.start[n]
|
||||||
|
size := uint32(len(m.b) - i)
|
||||||
|
binary.BigEndian.PutUint32(m.b[i:], size)
|
||||||
|
|
||||||
|
m.start = m.start[:n]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) Write(b []byte) {
|
||||||
|
m.b = append(m.b, b...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) WriteBytes(b ...byte) {
|
||||||
|
m.b = append(m.b, b...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) WriteString(s string) {
|
||||||
|
m.b = append(m.b, s...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) Skip(n int) {
|
||||||
|
m.b = append(m.b, make([]byte, n)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) WriteUint16(v uint16) {
|
||||||
|
m.b = append(m.b, byte(v>>8), byte(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) WriteUint24(v uint32) {
|
||||||
|
m.b = append(m.b, byte(v>>16), byte(v>>8), byte(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) WriteUint32(v uint32) {
|
||||||
|
m.b = append(m.b, byte(v>>24), byte(v>>16), byte(v>>8), byte(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) WriteUint64(v uint64) {
|
||||||
|
m.b = append(m.b, byte(v>>56), byte(v>>48), byte(v>>40), byte(v>>32), byte(v>>24), byte(v>>16), byte(v>>8), byte(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) WriteFloat16(f float64) {
|
||||||
|
i, f := math.Modf(f)
|
||||||
|
f *= 256
|
||||||
|
m.b = append(m.b, byte(i), byte(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) WriteFloat32(f float64) {
|
||||||
|
i, f := math.Modf(f)
|
||||||
|
f *= 65536
|
||||||
|
m.b = append(m.b, byte(uint16(i)>>8), byte(i), byte(uint16(f)>>8), byte(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Movie) WriteMatrix() {
|
||||||
|
m.WriteUint32(0x00010000)
|
||||||
|
m.Skip(4)
|
||||||
|
m.Skip(4)
|
||||||
|
m.Skip(4)
|
||||||
|
m.WriteUint32(0x00010000)
|
||||||
|
m.Skip(4)
|
||||||
|
m.Skip(4)
|
||||||
|
m.Skip(4)
|
||||||
|
m.WriteUint32(0x40000000)
|
||||||
|
}
|
@@ -1,19 +1,30 @@
|
|||||||
|
## Fragmented MP4
|
||||||
|
|
||||||
|
```
|
||||||
|
ffmpeg -i "rtsp://..." -movflags +frag_keyframe+separate_moof+default_base_moof+empty_moov -frag_duration 1 -c copy -t 5 sample.mp4
|
||||||
|
```
|
||||||
|
|
||||||
|
- movflags frag_keyframe
|
||||||
|
Start a new fragment at each video keyframe.
|
||||||
|
- frag_duration duration
|
||||||
|
Create fragments that are duration microseconds long.
|
||||||
|
- movflags separate_moof
|
||||||
|
Write a separate moof (movie fragment) atom for each track.
|
||||||
|
- movflags default_base_moof
|
||||||
|
Similarly to the omit_tfhd_offset, this flag avoids writing the absolute base_data_offset field in tfhd atoms, but does so by using the new default-base-is-moof flag instead.
|
||||||
|
|
||||||
|
https://ffmpeg.org/ffmpeg-formats.html#Options-13
|
||||||
|
|
||||||
## HEVC
|
## HEVC
|
||||||
|
|
||||||
Browser | avc1 | hvc1 | hev1
|
| Browser | avc1 | hvc1 | hev1 |
|
||||||
------------|------|------|---
|
|-------------|------|------|------|
|
||||||
Mac Chrome | + | - | +
|
| Mac Chrome | + | - | + |
|
||||||
Mac Safari | + | + | -
|
| Mac Safari | + | + | - |
|
||||||
iOS 15? | + | + | -
|
| iOS 15? | + | + | - |
|
||||||
Mac Firefox | + | - | -
|
| Mac Firefox | + | - | - |
|
||||||
iOS 12 | + | - | -
|
| iOS 12 | + | - | - |
|
||||||
Android 13 | + | - | -
|
| Android 13 | + | - | - |
|
||||||
|
|
||||||
```
|
|
||||||
ffmpeg -i input-hev1.mp4 -c:v copy -tag:v hvc1 -c:a copy output-hvc1.mp4
|
|
||||||
Stream #0:0(eng): Video: hevc (Main) (hev1 / 0x31766568), yuv420p(tv, progressive), 720x404, 164 kb/s, 29.97 fps,
|
|
||||||
Stream #0:0(eng): Video: hevc (Main) (hvc1 / 0x31637668), yuv420p(tv, progressive), 720x404, 164 kb/s, 29.97 fps,
|
|
||||||
```
|
|
||||||
|
|
||||||
## Useful links
|
## Useful links
|
||||||
|
|
||||||
|
@@ -24,6 +24,16 @@ type Consumer struct {
|
|||||||
send uint32
|
send uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseQuery - like usual parse, but with mp4 param handler
|
||||||
|
func ParseQuery(query map[string][]string) []*streamer.Media {
|
||||||
|
if query["mp4"] != nil {
|
||||||
|
cons := Consumer{}
|
||||||
|
return cons.GetMedias()
|
||||||
|
}
|
||||||
|
|
||||||
|
return streamer.ParseQuery(query)
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
waitNone byte = iota
|
waitNone byte = iota
|
||||||
waitKeyframe
|
waitKeyframe
|
||||||
@@ -140,14 +150,33 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
|||||||
push = wrapper(push)
|
push = wrapper(push)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return track.Bind(push)
|
||||||
|
|
||||||
|
case streamer.CodecOpus, streamer.CodecMP3, streamer.CodecPCMU, streamer.CodecPCMA:
|
||||||
|
push := func(packet *rtp.Packet) error {
|
||||||
|
if c.wait != waitNone {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := c.muxer.Marshal(trackID, packet)
|
||||||
|
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||||
|
c.Fire(buf)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return track.Bind(push)
|
return track.Bind(push)
|
||||||
}
|
}
|
||||||
|
|
||||||
panic("unsupported codec")
|
panic("unsupported codec")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) MimeCodecs() string {
|
||||||
|
return c.muxer.MimeCodecs(c.codecs)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Consumer) MimeType() string {
|
func (c *Consumer) MimeType() string {
|
||||||
return c.muxer.MimeType(c.codecs)
|
return `video/mp4; codecs="` + c.MimeCodecs() + `"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Consumer) Init() ([]byte, error) {
|
func (c *Consumer) Init() ([]byte, error) {
|
||||||
|
221
pkg/mp4/muxer.go
221
pkg/mp4/muxer.go
@@ -1,17 +1,13 @@
|
|||||||
package mp4
|
package mp4
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"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/iso"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"github.com/deepch/vdk/av"
|
|
||||||
"github.com/deepch/vdk/codec/h264parser"
|
"github.com/deepch/vdk/codec/h264parser"
|
||||||
"github.com/deepch/vdk/codec/h265parser"
|
"github.com/deepch/vdk/codec/h265parser"
|
||||||
"github.com/deepch/vdk/format/fmp4/fmp4io"
|
|
||||||
"github.com/deepch/vdk/format/mp4/mp4io"
|
|
||||||
"github.com/deepch/vdk/format/mp4f/mp4fio"
|
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,8 +17,15 @@ type Muxer struct {
|
|||||||
pts []uint32
|
pts []uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
|
const (
|
||||||
s := `video/mp4; codecs="`
|
MimeH264 = "avc1.640029"
|
||||||
|
MimeH265 = "hvc1.1.6.L153.B0"
|
||||||
|
MimeAAC = "mp4a.40.2"
|
||||||
|
MimeOpus = "opus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Muxer) MimeCodecs(codecs []*streamer.Codec) string {
|
||||||
|
var s string
|
||||||
|
|
||||||
for i, codec := range codecs {
|
for i, codec := range codecs {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
@@ -35,17 +38,23 @@ func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
|
|||||||
case streamer.CodecH265:
|
case streamer.CodecH265:
|
||||||
// H.265 profile=main level=5.1
|
// H.265 profile=main level=5.1
|
||||||
// hvc1 - supported in Safari, hev1 - doesn't, both supported in Chrome
|
// hvc1 - supported in Safari, hev1 - doesn't, both supported in Chrome
|
||||||
s += "hvc1.1.6.L153.B0"
|
s += MimeH265
|
||||||
case streamer.CodecAAC:
|
case streamer.CodecAAC:
|
||||||
s += "mp4a.40.2"
|
s += MimeAAC
|
||||||
|
case streamer.CodecOpus:
|
||||||
|
s += MimeOpus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return s + `"`
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
||||||
moov := MOOV()
|
mv := iso.NewMovie(1024)
|
||||||
|
mv.WriteFileType()
|
||||||
|
|
||||||
|
mv.StartAtom(iso.Moov)
|
||||||
|
mv.WriteMovieHeader()
|
||||||
|
|
||||||
for i, codec := range codecs {
|
for i, codec := range codecs {
|
||||||
switch codec.Name {
|
switch codec.Name {
|
||||||
@@ -62,35 +71,11 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
width := codecData.Width()
|
mv.WriteVideoTrack(
|
||||||
height := codecData.Height()
|
uint32(i+1), codec.Name, codec.ClockRate,
|
||||||
|
uint16(codecData.Width()), uint16(codecData.Height()),
|
||||||
trak := TRAK(i + 1)
|
codecData.AVCDecoderConfRecordBytes(),
|
||||||
trak.Header.TrackWidth = float64(width)
|
)
|
||||||
trak.Header.TrackHeight = float64(height)
|
|
||||||
trak.Media.Header.TimeScale = int32(codec.ClockRate)
|
|
||||||
trak.Media.Handler = &mp4io.HandlerRefer{
|
|
||||||
SubType: [4]byte{'v', 'i', 'd', 'e'},
|
|
||||||
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
|
|
||||||
}
|
|
||||||
trak.Media.Info.Video = &mp4io.VideoMediaInfo{
|
|
||||||
Flags: 0x000001,
|
|
||||||
}
|
|
||||||
trak.Media.Info.Sample.SampleDesc.AVC1Desc = &mp4io.AVC1Desc{
|
|
||||||
DataRefIdx: 1,
|
|
||||||
HorizontalResolution: 72,
|
|
||||||
VorizontalResolution: 72,
|
|
||||||
Width: int16(width),
|
|
||||||
Height: int16(height),
|
|
||||||
FrameCount: 1,
|
|
||||||
Depth: 24,
|
|
||||||
ColorTableId: -1,
|
|
||||||
Conf: &mp4io.AVC1Conf{
|
|
||||||
Data: codecData.AVCDecoderConfRecordBytes(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
moov.Tracks = append(moov.Tracks, trak)
|
|
||||||
|
|
||||||
case streamer.CodecH265:
|
case streamer.CodecH265:
|
||||||
vps, sps, pps := h265.GetParameterSet(codec.FmtpLine)
|
vps, sps, pps := h265.GetParameterSet(codec.FmtpLine)
|
||||||
@@ -106,35 +91,11 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
width := codecData.Width()
|
mv.WriteVideoTrack(
|
||||||
height := codecData.Height()
|
uint32(i+1), codec.Name, codec.ClockRate,
|
||||||
|
uint16(codecData.Width()), uint16(codecData.Height()),
|
||||||
trak := TRAK(i + 1)
|
codecData.AVCDecoderConfRecordBytes(),
|
||||||
trak.Header.TrackWidth = float64(width)
|
)
|
||||||
trak.Header.TrackHeight = float64(height)
|
|
||||||
trak.Media.Header.TimeScale = int32(codec.ClockRate)
|
|
||||||
trak.Media.Handler = &mp4io.HandlerRefer{
|
|
||||||
SubType: [4]byte{'v', 'i', 'd', 'e'},
|
|
||||||
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
|
|
||||||
}
|
|
||||||
trak.Media.Info.Video = &mp4io.VideoMediaInfo{
|
|
||||||
Flags: 0x000001,
|
|
||||||
}
|
|
||||||
trak.Media.Info.Sample.SampleDesc.HV1Desc = &mp4io.HV1Desc{
|
|
||||||
DataRefIdx: 1,
|
|
||||||
HorizontalResolution: 72,
|
|
||||||
VorizontalResolution: 72,
|
|
||||||
Width: int16(width),
|
|
||||||
Height: int16(height),
|
|
||||||
FrameCount: 1,
|
|
||||||
Depth: 24,
|
|
||||||
ColorTableId: -1,
|
|
||||||
Conf: &mp4io.HV1Conf{
|
|
||||||
Data: codecData.AVCDecoderConfRecordBytes(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
moov.Tracks = append(moov.Tracks, trak)
|
|
||||||
|
|
||||||
case streamer.CodecAAC:
|
case streamer.CodecAAC:
|
||||||
s := streamer.Between(codec.FmtpLine, "config=", ";")
|
s := streamer.Between(codec.FmtpLine, "config=", ";")
|
||||||
@@ -143,44 +104,29 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
trak := TRAK(i + 1)
|
mv.WriteAudioTrack(
|
||||||
trak.Header.AlternateGroup = 1
|
uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, b,
|
||||||
trak.Header.Duration = 0
|
)
|
||||||
trak.Header.Volume = 1
|
|
||||||
trak.Media.Header.TimeScale = int32(codec.ClockRate)
|
|
||||||
|
|
||||||
trak.Media.Handler = &mp4io.HandlerRefer{
|
case streamer.CodecOpus, streamer.CodecMP3, streamer.CodecPCMU, streamer.CodecPCMA:
|
||||||
SubType: [4]byte{'s', 'o', 'u', 'n'},
|
mv.WriteAudioTrack(
|
||||||
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
|
uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, nil,
|
||||||
}
|
)
|
||||||
trak.Media.Info.Sound = &mp4io.SoundMediaInfo{}
|
|
||||||
|
|
||||||
trak.Media.Info.Sample.SampleDesc.MP4ADesc = &mp4io.MP4ADesc{
|
|
||||||
DataRefIdx: 1,
|
|
||||||
NumberOfChannels: int16(codec.Channels),
|
|
||||||
SampleSize: int16(av.FLTP.BytesPerSample() * 4),
|
|
||||||
SampleRate: float64(codec.ClockRate),
|
|
||||||
Unknowns: []mp4io.Atom{ESDS(b)},
|
|
||||||
}
|
|
||||||
|
|
||||||
moov.Tracks = append(moov.Tracks, trak)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
trex := &mp4io.TrackExtend{
|
|
||||||
TrackId: uint32(i + 1),
|
|
||||||
DefaultSampleDescIdx: 1,
|
|
||||||
DefaultSampleDuration: 0,
|
|
||||||
}
|
|
||||||
moov.MovieExtend.Tracks = append(moov.MovieExtend.Tracks, trex)
|
|
||||||
|
|
||||||
m.pts = append(m.pts, 0)
|
m.pts = append(m.pts, 0)
|
||||||
m.dts = append(m.dts, 0)
|
m.dts = append(m.dts, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
data := make([]byte, moov.Len())
|
mv.StartAtom(iso.MoovMvex)
|
||||||
moov.Marshal(data)
|
for i := range codecs {
|
||||||
|
mv.WriteTrackExtend(uint32(i + 1))
|
||||||
|
}
|
||||||
|
mv.EndAtom() // MVEX
|
||||||
|
|
||||||
return append(FTYP(), data...), nil
|
mv.EndAtom() // MOOV
|
||||||
|
|
||||||
|
return mv.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Muxer) Reset() {
|
func (m *Muxer) Reset() {
|
||||||
@@ -192,65 +138,28 @@ func (m *Muxer) Reset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Muxer) Marshal(trackID byte, packet *rtp.Packet) []byte {
|
func (m *Muxer) Marshal(trackID byte, packet *rtp.Packet) []byte {
|
||||||
run := &mp4fio.TrackFragRun{
|
// important before increment
|
||||||
Flags: 0x000b05,
|
time := m.dts[trackID]
|
||||||
FirstSampleFlags: uint32(fmp4io.SampleNoDependencies),
|
|
||||||
DataOffset: 0,
|
|
||||||
Entries: []mp4io.TrackFragRunEntry{},
|
|
||||||
}
|
|
||||||
|
|
||||||
moof := &mp4fio.MovieFrag{
|
|
||||||
Header: &mp4fio.MovieFragHeader{
|
|
||||||
Seqnum: m.fragIndex + 1,
|
|
||||||
},
|
|
||||||
Tracks: []*mp4fio.TrackFrag{
|
|
||||||
{
|
|
||||||
Header: &mp4fio.TrackFragHeader{
|
|
||||||
Data: []byte{0x00, 0x02, 0x00, 0x20, 0x00, 0x00, 0x00, trackID + 1, 0x01, 0x01, 0x00, 0x00},
|
|
||||||
},
|
|
||||||
DecodeTime: &mp4fio.TrackFragDecodeTime{
|
|
||||||
Version: 1,
|
|
||||||
Flags: 0,
|
|
||||||
Time: m.dts[trackID],
|
|
||||||
},
|
|
||||||
Run: run,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
entry := mp4io.TrackFragRunEntry{
|
|
||||||
Size: uint32(len(packet.Payload)),
|
|
||||||
}
|
|
||||||
|
|
||||||
newTime := packet.Timestamp
|
|
||||||
if m.pts[trackID] > 0 {
|
|
||||||
entry.Duration = newTime - m.pts[trackID]
|
|
||||||
m.dts[trackID] += uint64(entry.Duration)
|
|
||||||
} else {
|
|
||||||
// important, or Safari will fail with first frame
|
|
||||||
entry.Duration = 1
|
|
||||||
}
|
|
||||||
m.pts[trackID] = newTime
|
|
||||||
|
|
||||||
// important before moof.Len()
|
|
||||||
run.Entries = append(run.Entries, entry)
|
|
||||||
|
|
||||||
moofLen := moof.Len()
|
|
||||||
mdatLen := 8 + len(packet.Payload)
|
|
||||||
|
|
||||||
// important after moof.Len()
|
|
||||||
run.DataOffset = uint32(moofLen + 8)
|
|
||||||
|
|
||||||
buf := make([]byte, moofLen+mdatLen)
|
|
||||||
moof.Marshal(buf)
|
|
||||||
|
|
||||||
binary.BigEndian.PutUint32(buf[moofLen:], uint32(mdatLen))
|
|
||||||
copy(buf[moofLen+4:], "mdat")
|
|
||||||
copy(buf[moofLen+8:], packet.Payload)
|
|
||||||
|
|
||||||
m.fragIndex++
|
m.fragIndex++
|
||||||
|
|
||||||
//m.total += moofLen + mdatLen
|
var duration uint32
|
||||||
|
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
|
||||||
|
|
||||||
return buf
|
mv := iso.NewMovie(1024 + len(packet.Payload))
|
||||||
|
mv.WriteMovieFragment(
|
||||||
|
m.fragIndex, uint32(trackID+1), duration,
|
||||||
|
uint32(len(packet.Payload)), time,
|
||||||
|
)
|
||||||
|
mv.WriteData(packet.Payload)
|
||||||
|
|
||||||
|
return mv.Bytes()
|
||||||
}
|
}
|
||||||
|
@@ -50,7 +50,7 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
c.MimeType = muxer.MimeType(codecs)
|
c.MimeType = `video/mp4; codecs="` + muxer.MimeCodecs(codecs) + `"`
|
||||||
|
|
||||||
switch track.Codec.Name {
|
switch track.Codec.Name {
|
||||||
case streamer.CodecH264:
|
case streamer.CodecH264:
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
package mp4f
|
package mp4
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"github.com/deepch/vdk/av"
|
"github.com/deepch/vdk/av"
|
||||||
@@ -101,7 +103,17 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
|||||||
return track.Bind(push)
|
return track.Bind(push)
|
||||||
|
|
||||||
case streamer.CodecAAC:
|
case streamer.CodecAAC:
|
||||||
stream, _ := aacparser.NewCodecDataFromMPEG4AudioConfigBytes([]byte{20, 8})
|
s := streamer.Between(codec.FmtpLine, "config=", ";")
|
||||||
|
|
||||||
|
b, err := hex.DecodeString(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
stream, err := aacparser.NewCodecDataFromMPEG4AudioConfigBytes(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
c.mimeType += ",mp4a.40.2"
|
c.mimeType += ",mp4a.40.2"
|
||||||
c.streams = append(c.streams, stream)
|
c.streams = append(c.streams, stream)
|
||||||
@@ -131,6 +143,11 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if codec.IsRTP() {
|
||||||
|
wrapper := aac.RTPDepay(track)
|
||||||
|
push = wrapper(push)
|
||||||
|
}
|
||||||
|
|
||||||
return track.Bind(push)
|
return track.Bind(push)
|
||||||
}
|
}
|
||||||
|
|
174
pkg/mp4/v2/consumer.go
Normal file
174
pkg/mp4/v2/consumer.go
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
package mp4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Consumer struct {
|
||||||
|
streamer.Element
|
||||||
|
|
||||||
|
Medias []*streamer.Media
|
||||||
|
UserAgent string
|
||||||
|
RemoteAddr string
|
||||||
|
|
||||||
|
muxer *Muxer
|
||||||
|
codecs []*streamer.Codec
|
||||||
|
wait byte
|
||||||
|
|
||||||
|
send uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
waitNone byte = iota
|
||||||
|
waitKeyframe
|
||||||
|
waitInit
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Consumer) GetMedias() []*streamer.Media {
|
||||||
|
if c.Medias != nil {
|
||||||
|
return c.Medias
|
||||||
|
}
|
||||||
|
|
||||||
|
// default medias
|
||||||
|
return []*streamer.Media{
|
||||||
|
{
|
||||||
|
Kind: streamer.KindVideo,
|
||||||
|
Direction: streamer.DirectionRecvonly,
|
||||||
|
Codecs: []*streamer.Codec{
|
||||||
|
{Name: streamer.CodecH264},
|
||||||
|
{Name: streamer.CodecH265},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Kind: streamer.KindAudio,
|
||||||
|
Direction: streamer.DirectionRecvonly,
|
||||||
|
Codecs: []*streamer.Codec{
|
||||||
|
{Name: streamer.CodecAAC},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||||
|
trackID := byte(len(c.codecs))
|
||||||
|
c.codecs = append(c.codecs, track.Codec)
|
||||||
|
|
||||||
|
codec := track.Codec
|
||||||
|
switch codec.Name {
|
||||||
|
case streamer.CodecH264:
|
||||||
|
c.wait = waitInit
|
||||||
|
|
||||||
|
push := func(packet *rtp.Packet) error {
|
||||||
|
if packet.Version != h264.RTPPacketVersionAVC {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.wait != waitNone {
|
||||||
|
if c.wait == waitInit || !h264.IsKeyframe(packet.Payload) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c.wait = waitNone
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := c.muxer.Marshal(trackID, packet)
|
||||||
|
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||||
|
c.Fire(buf)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var wrapper streamer.WrapperFunc
|
||||||
|
if codec.IsRTP() {
|
||||||
|
wrapper = h264.RTPDepay(track)
|
||||||
|
} else {
|
||||||
|
wrapper = h264.RepairAVC(track)
|
||||||
|
}
|
||||||
|
push = wrapper(push)
|
||||||
|
|
||||||
|
return track.Bind(push)
|
||||||
|
|
||||||
|
case streamer.CodecH265:
|
||||||
|
c.wait = waitInit
|
||||||
|
|
||||||
|
push := func(packet *rtp.Packet) error {
|
||||||
|
if packet.Version != h264.RTPPacketVersionAVC {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.wait != waitNone {
|
||||||
|
if c.wait == waitInit || !h265.IsKeyframe(packet.Payload) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c.wait = waitNone
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := c.muxer.Marshal(trackID, packet)
|
||||||
|
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||||
|
c.Fire(buf)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if codec.IsRTP() {
|
||||||
|
wrapper := h265.RTPDepay(track)
|
||||||
|
push = wrapper(push)
|
||||||
|
}
|
||||||
|
|
||||||
|
return track.Bind(push)
|
||||||
|
|
||||||
|
case streamer.CodecAAC:
|
||||||
|
push := func(packet *rtp.Packet) error {
|
||||||
|
if c.wait != waitNone {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := c.muxer.Marshal(trackID, packet)
|
||||||
|
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||||
|
c.Fire(buf)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if codec.IsRTP() {
|
||||||
|
wrapper := aac.RTPDepay(track)
|
||||||
|
push = wrapper(push)
|
||||||
|
}
|
||||||
|
|
||||||
|
return track.Bind(push)
|
||||||
|
}
|
||||||
|
|
||||||
|
panic("unsupported codec")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) MimeType() string {
|
||||||
|
return c.muxer.MimeType(c.codecs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) Init() ([]byte, error) {
|
||||||
|
c.muxer = &Muxer{}
|
||||||
|
return c.muxer.GetInit(c.codecs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) Start() {
|
||||||
|
if c.wait == waitInit {
|
||||||
|
c.wait = waitKeyframe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
func (c *Consumer) MarshalJSON() ([]byte, error) {
|
||||||
|
info := &streamer.Info{
|
||||||
|
Type: "MP4 client",
|
||||||
|
RemoteAddr: c.RemoteAddr,
|
||||||
|
UserAgent: c.UserAgent,
|
||||||
|
Send: atomic.LoadUint32(&c.send),
|
||||||
|
}
|
||||||
|
return json.Marshal(info)
|
||||||
|
}
|
256
pkg/mp4/v2/muxer.go
Normal file
256
pkg/mp4/v2/muxer.go
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
package mp4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
|
"github.com/deepch/vdk/av"
|
||||||
|
"github.com/deepch/vdk/codec/h264parser"
|
||||||
|
"github.com/deepch/vdk/codec/h265parser"
|
||||||
|
"github.com/deepch/vdk/format/fmp4/fmp4io"
|
||||||
|
"github.com/deepch/vdk/format/mp4/mp4io"
|
||||||
|
"github.com/deepch/vdk/format/mp4f/mp4fio"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Muxer struct {
|
||||||
|
fragIndex uint32
|
||||||
|
dts []uint64
|
||||||
|
pts []uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
|
||||||
|
s := `video/mp4; codecs="`
|
||||||
|
|
||||||
|
for i, codec := range codecs {
|
||||||
|
if i > 0 {
|
||||||
|
s += ","
|
||||||
|
}
|
||||||
|
|
||||||
|
switch codec.Name {
|
||||||
|
case streamer.CodecH264:
|
||||||
|
s += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
|
||||||
|
case streamer.CodecH265:
|
||||||
|
// H.265 profile=main level=5.1
|
||||||
|
// hvc1 - supported in Safari, hev1 - doesn't, both supported in Chrome
|
||||||
|
s += "hvc1.1.6.L153.B0"
|
||||||
|
case streamer.CodecAAC:
|
||||||
|
s += "mp4a.40.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s + `"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
||||||
|
moov := MOOV()
|
||||||
|
|
||||||
|
for i, codec := range codecs {
|
||||||
|
switch codec.Name {
|
||||||
|
case streamer.CodecH264:
|
||||||
|
sps, pps := h264.GetParameterSet(codec.FmtpLine)
|
||||||
|
if sps == nil {
|
||||||
|
// some dummy SPS and PPS not a problem
|
||||||
|
sps = []byte{0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2}
|
||||||
|
pps = []byte{0x68, 0xce, 0x38, 0x80}
|
||||||
|
}
|
||||||
|
|
||||||
|
codecData, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
width := codecData.Width()
|
||||||
|
height := codecData.Height()
|
||||||
|
|
||||||
|
trak := TRAK(i + 1)
|
||||||
|
trak.Header.TrackWidth = float64(width)
|
||||||
|
trak.Header.TrackHeight = float64(height)
|
||||||
|
trak.Media.Header.TimeScale = int32(codec.ClockRate)
|
||||||
|
trak.Media.Handler = &mp4io.HandlerRefer{
|
||||||
|
SubType: [4]byte{'v', 'i', 'd', 'e'},
|
||||||
|
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
|
||||||
|
}
|
||||||
|
trak.Media.Info.Video = &mp4io.VideoMediaInfo{
|
||||||
|
Flags: 0x000001,
|
||||||
|
}
|
||||||
|
trak.Media.Info.Sample.SampleDesc.AVC1Desc = &mp4io.AVC1Desc{
|
||||||
|
DataRefIdx: 1,
|
||||||
|
HorizontalResolution: 72,
|
||||||
|
VorizontalResolution: 72,
|
||||||
|
Width: int16(width),
|
||||||
|
Height: int16(height),
|
||||||
|
FrameCount: 1,
|
||||||
|
Depth: 24,
|
||||||
|
ColorTableId: -1,
|
||||||
|
Conf: &mp4io.AVC1Conf{
|
||||||
|
Data: codecData.AVCDecoderConfRecordBytes(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
moov.Tracks = append(moov.Tracks, trak)
|
||||||
|
|
||||||
|
case streamer.CodecH265:
|
||||||
|
vps, sps, pps := h265.GetParameterSet(codec.FmtpLine)
|
||||||
|
if sps == nil {
|
||||||
|
// some dummy SPS and PPS not a problem
|
||||||
|
vps = []byte{0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x99, 0xac, 0x09}
|
||||||
|
sps = []byte{0x42, 0x01, 0x01, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x99, 0xa0, 0x01, 0x40, 0x20, 0x05, 0xa1, 0xfe, 0x5a, 0xee, 0x46, 0xc1, 0xae, 0x55, 0x04}
|
||||||
|
pps = []byte{0x44, 0x01, 0xc0, 0x73, 0xc0, 0x4c, 0x90}
|
||||||
|
}
|
||||||
|
|
||||||
|
codecData, err := h265parser.NewCodecDataFromVPSAndSPSAndPPS(vps, sps, pps)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
width := codecData.Width()
|
||||||
|
height := codecData.Height()
|
||||||
|
|
||||||
|
trak := TRAK(i + 1)
|
||||||
|
trak.Header.TrackWidth = float64(width)
|
||||||
|
trak.Header.TrackHeight = float64(height)
|
||||||
|
trak.Media.Header.TimeScale = int32(codec.ClockRate)
|
||||||
|
trak.Media.Handler = &mp4io.HandlerRefer{
|
||||||
|
SubType: [4]byte{'v', 'i', 'd', 'e'},
|
||||||
|
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
|
||||||
|
}
|
||||||
|
trak.Media.Info.Video = &mp4io.VideoMediaInfo{
|
||||||
|
Flags: 0x000001,
|
||||||
|
}
|
||||||
|
trak.Media.Info.Sample.SampleDesc.HV1Desc = &mp4io.HV1Desc{
|
||||||
|
DataRefIdx: 1,
|
||||||
|
HorizontalResolution: 72,
|
||||||
|
VorizontalResolution: 72,
|
||||||
|
Width: int16(width),
|
||||||
|
Height: int16(height),
|
||||||
|
FrameCount: 1,
|
||||||
|
Depth: 24,
|
||||||
|
ColorTableId: -1,
|
||||||
|
Conf: &mp4io.HV1Conf{
|
||||||
|
Data: codecData.AVCDecoderConfRecordBytes(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
moov.Tracks = append(moov.Tracks, trak)
|
||||||
|
|
||||||
|
case streamer.CodecAAC:
|
||||||
|
s := streamer.Between(codec.FmtpLine, "config=", ";")
|
||||||
|
b, err := hex.DecodeString(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
trak := TRAK(i + 1)
|
||||||
|
trak.Header.AlternateGroup = 1
|
||||||
|
trak.Header.Duration = 0
|
||||||
|
trak.Header.Volume = 1
|
||||||
|
trak.Media.Header.TimeScale = int32(codec.ClockRate)
|
||||||
|
|
||||||
|
trak.Media.Handler = &mp4io.HandlerRefer{
|
||||||
|
SubType: [4]byte{'s', 'o', 'u', 'n'},
|
||||||
|
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
|
||||||
|
}
|
||||||
|
trak.Media.Info.Sound = &mp4io.SoundMediaInfo{}
|
||||||
|
|
||||||
|
trak.Media.Info.Sample.SampleDesc.MP4ADesc = &mp4io.MP4ADesc{
|
||||||
|
DataRefIdx: 1,
|
||||||
|
NumberOfChannels: int16(codec.Channels),
|
||||||
|
SampleSize: int16(av.FLTP.BytesPerSample() * 4),
|
||||||
|
SampleRate: float64(codec.ClockRate),
|
||||||
|
Unknowns: []mp4io.Atom{ESDS(b)},
|
||||||
|
}
|
||||||
|
|
||||||
|
moov.Tracks = append(moov.Tracks, trak)
|
||||||
|
}
|
||||||
|
|
||||||
|
trex := &mp4io.TrackExtend{
|
||||||
|
TrackId: uint32(i + 1),
|
||||||
|
DefaultSampleDescIdx: 1,
|
||||||
|
DefaultSampleDuration: 0,
|
||||||
|
}
|
||||||
|
moov.MovieExtend.Tracks = append(moov.MovieExtend.Tracks, trex)
|
||||||
|
|
||||||
|
m.pts = append(m.pts, 0)
|
||||||
|
m.dts = append(m.dts, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]byte, moov.Len())
|
||||||
|
moov.Marshal(data)
|
||||||
|
|
||||||
|
return append(FTYP(), data...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Muxer) Reset() {
|
||||||
|
m.fragIndex = 0
|
||||||
|
for i := range m.dts {
|
||||||
|
m.dts[i] = 0
|
||||||
|
m.pts[i] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Muxer) Marshal(trackID byte, packet *rtp.Packet) []byte {
|
||||||
|
run := &mp4fio.TrackFragRun{
|
||||||
|
Flags: 0x000b05,
|
||||||
|
FirstSampleFlags: uint32(fmp4io.SampleNoDependencies),
|
||||||
|
DataOffset: 0,
|
||||||
|
Entries: []mp4io.TrackFragRunEntry{},
|
||||||
|
}
|
||||||
|
|
||||||
|
moof := &mp4fio.MovieFrag{
|
||||||
|
Header: &mp4fio.MovieFragHeader{
|
||||||
|
Seqnum: m.fragIndex + 1,
|
||||||
|
},
|
||||||
|
Tracks: []*mp4fio.TrackFrag{
|
||||||
|
{
|
||||||
|
Header: &mp4fio.TrackFragHeader{
|
||||||
|
Data: []byte{0x00, 0x02, 0x00, 0x20, 0x00, 0x00, 0x00, trackID + 1, 0x01, 0x01, 0x00, 0x00},
|
||||||
|
},
|
||||||
|
DecodeTime: &mp4fio.TrackFragDecodeTime{
|
||||||
|
Version: 1,
|
||||||
|
Flags: 0,
|
||||||
|
Time: m.dts[trackID],
|
||||||
|
},
|
||||||
|
Run: run,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := mp4io.TrackFragRunEntry{
|
||||||
|
Size: uint32(len(packet.Payload)),
|
||||||
|
}
|
||||||
|
|
||||||
|
newTime := packet.Timestamp
|
||||||
|
if m.pts[trackID] > 0 {
|
||||||
|
entry.Duration = newTime - m.pts[trackID]
|
||||||
|
m.dts[trackID] += uint64(entry.Duration)
|
||||||
|
} else {
|
||||||
|
// important, or Safari will fail with first frame
|
||||||
|
entry.Duration = 1
|
||||||
|
}
|
||||||
|
m.pts[trackID] = newTime
|
||||||
|
|
||||||
|
// important before moof.Len()
|
||||||
|
run.Entries = append(run.Entries, entry)
|
||||||
|
|
||||||
|
moofLen := moof.Len()
|
||||||
|
mdatLen := 8 + len(packet.Payload)
|
||||||
|
|
||||||
|
// important after moof.Len()
|
||||||
|
run.DataOffset = uint32(moofLen + 8)
|
||||||
|
|
||||||
|
buf := make([]byte, moofLen+mdatLen)
|
||||||
|
moof.Marshal(buf)
|
||||||
|
|
||||||
|
binary.BigEndian.PutUint32(buf[moofLen:], uint32(mdatLen))
|
||||||
|
copy(buf[moofLen+4:], "mdat")
|
||||||
|
copy(buf[moofLen+8:], packet.Payload)
|
||||||
|
|
||||||
|
m.fragIndex++
|
||||||
|
|
||||||
|
//m.total += moofLen + mdatLen
|
||||||
|
|
||||||
|
return buf
|
||||||
|
}
|
143
pkg/mp4/v2/segment.go
Normal file
143
pkg/mp4/v2/segment.go
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package mp4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Segment struct {
|
||||||
|
streamer.Element
|
||||||
|
|
||||||
|
Medias []*streamer.Media
|
||||||
|
UserAgent string
|
||||||
|
RemoteAddr string
|
||||||
|
|
||||||
|
MimeType string
|
||||||
|
OnlyKeyframe bool
|
||||||
|
|
||||||
|
send uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Segment) GetMedias() []*streamer.Media {
|
||||||
|
if c.Medias != nil {
|
||||||
|
return c.Medias
|
||||||
|
}
|
||||||
|
|
||||||
|
// default medias
|
||||||
|
return []*streamer.Media{
|
||||||
|
{
|
||||||
|
Kind: streamer.KindVideo,
|
||||||
|
Direction: streamer.DirectionRecvonly,
|
||||||
|
Codecs: []*streamer.Codec{
|
||||||
|
{Name: streamer.CodecH264},
|
||||||
|
{Name: streamer.CodecH265},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||||
|
muxer := &Muxer{}
|
||||||
|
|
||||||
|
codecs := []*streamer.Codec{track.Codec}
|
||||||
|
|
||||||
|
init, err := muxer.GetInit(codecs)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.MimeType = muxer.MimeType(codecs)
|
||||||
|
|
||||||
|
switch track.Codec.Name {
|
||||||
|
case streamer.CodecH264:
|
||||||
|
var push streamer.WriterFunc
|
||||||
|
|
||||||
|
if c.OnlyKeyframe {
|
||||||
|
push = func(packet *rtp.Packet) error {
|
||||||
|
if !h264.IsKeyframe(packet.Payload) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := muxer.Marshal(0, packet)
|
||||||
|
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||||
|
c.Fire(append(init, buf...))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var buf []byte
|
||||||
|
|
||||||
|
push = func(packet *rtp.Packet) error {
|
||||||
|
if h264.IsKeyframe(packet.Payload) {
|
||||||
|
// fist frame - send only IFrame
|
||||||
|
// other frames - send IFrame and all PFrames
|
||||||
|
if buf == nil {
|
||||||
|
buf = append(buf, init...)
|
||||||
|
b := muxer.Marshal(0, packet)
|
||||||
|
buf = append(buf, b...)
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||||
|
c.Fire(buf)
|
||||||
|
|
||||||
|
buf = buf[:0]
|
||||||
|
buf = append(buf, init...)
|
||||||
|
muxer.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf != nil {
|
||||||
|
b := muxer.Marshal(0, packet)
|
||||||
|
buf = append(buf, b...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var wrapper streamer.WrapperFunc
|
||||||
|
if track.Codec.IsRTP() {
|
||||||
|
wrapper = h264.RTPDepay(track)
|
||||||
|
} else {
|
||||||
|
wrapper = h264.RepairAVC(track)
|
||||||
|
}
|
||||||
|
push = wrapper(push)
|
||||||
|
|
||||||
|
return track.Bind(push)
|
||||||
|
|
||||||
|
case streamer.CodecH265:
|
||||||
|
push := func(packet *rtp.Packet) error {
|
||||||
|
if !h265.IsKeyframe(packet.Payload) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := muxer.Marshal(0, packet)
|
||||||
|
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||||
|
c.Fire(append(init, buf...))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if track.Codec.IsRTP() {
|
||||||
|
wrapper := h265.RTPDepay(track)
|
||||||
|
push = wrapper(push)
|
||||||
|
}
|
||||||
|
|
||||||
|
return track.Bind(push)
|
||||||
|
}
|
||||||
|
|
||||||
|
panic("unsupported codec")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Segment) MarshalJSON() ([]byte, error) {
|
||||||
|
info := &streamer.Info{
|
||||||
|
Type: "WS/MP4 client",
|
||||||
|
RemoteAddr: c.RemoteAddr,
|
||||||
|
UserAgent: c.UserAgent,
|
||||||
|
Send: atomic.LoadUint32(&c.send),
|
||||||
|
}
|
||||||
|
return json.Marshal(info)
|
||||||
|
}
|
@@ -347,7 +347,7 @@ func (c *Conn) Describe() error {
|
|||||||
|
|
||||||
func (c *Conn) Setup() error {
|
func (c *Conn) Setup() error {
|
||||||
for _, media := range c.Medias {
|
for _, media := range c.Medias {
|
||||||
_, err := c.SetupMedia(media, media.Codecs[0])
|
_, err := c.SetupMedia(media, media.Codecs[0], true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -356,11 +356,12 @@ func (c *Conn) Setup() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) SetupMedia(
|
func (c *Conn) SetupMedia(media *streamer.Media, codec *streamer.Codec, first bool) (*streamer.Track, error) {
|
||||||
media *streamer.Media, codec *streamer.Codec,
|
// TODO: rewrite recoonection and first flag
|
||||||
) (*streamer.Track, error) {
|
if first {
|
||||||
c.stateMu.Lock()
|
c.stateMu.Lock()
|
||||||
defer c.stateMu.Unlock()
|
defer c.stateMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
if c.state != StateConn && c.state != StateSetup {
|
if c.state != StateConn && c.state != StateSetup {
|
||||||
return nil, fmt.Errorf("RTSP SETUP from wrong state: %s", c.state)
|
return nil, fmt.Errorf("RTSP SETUP from wrong state: %s", c.state)
|
||||||
@@ -412,7 +413,7 @@ func (c *Conn) SetupMedia(
|
|||||||
|
|
||||||
for _, newMedia := range c.Medias {
|
for _, newMedia := range c.Medias {
|
||||||
if newMedia.Control == media.Control {
|
if newMedia.Control == media.Control {
|
||||||
return c.SetupMedia(newMedia, newMedia.Codecs[0])
|
return c.SetupMedia(newMedia, newMedia.Codecs[0], false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -47,9 +47,14 @@ func UnmarshalSDP(rawSDP []byte) ([]*streamer.Media, error) {
|
|||||||
return medias, nil
|
return medias, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// urlParse fix bug in URL from D-Link camera:
|
// urlParse fix bugs:
|
||||||
// Content-Base: rtsp://::ffff:192.168.1.123/onvif/profile.1/
|
// 1. Content-Base: rtsp://::ffff:192.168.1.123/onvif/profile.1/
|
||||||
|
// 2. Content-Base: rtsp://rtsp://turret2-cam.lan:554/stream1/
|
||||||
func urlParse(rawURL string) (*url.URL, error) {
|
func urlParse(rawURL string) (*url.URL, error) {
|
||||||
|
if strings.HasPrefix(rawURL, "rtsp://rtsp://") {
|
||||||
|
rawURL = rawURL[7:]
|
||||||
|
}
|
||||||
|
|
||||||
u, err := url.Parse(rawURL)
|
u, err := url.Parse(rawURL)
|
||||||
if err != nil && strings.HasSuffix(err.Error(), "after host") {
|
if err != nil && strings.HasSuffix(err.Error(), "after host") {
|
||||||
if i1 := strings.Index(rawURL, "://"); i1 > 0 {
|
if i1 := strings.Index(rawURL, "://"); i1 > 0 {
|
||||||
|
@@ -6,7 +6,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestURLParse(t *testing.T) {
|
func TestURLParse(t *testing.T) {
|
||||||
|
// https://github.com/AlexxIT/WebRTC/issues/395
|
||||||
base := "rtsp://::ffff:192.168.1.123/onvif/profile.1/"
|
base := "rtsp://::ffff:192.168.1.123/onvif/profile.1/"
|
||||||
_, err := urlParse(base)
|
u, err := urlParse(base)
|
||||||
assert.Empty(t, err)
|
assert.Empty(t, err)
|
||||||
|
assert.Equal(t, "::ffff:192.168.1.123:", u.Host)
|
||||||
|
|
||||||
|
// https://github.com/AlexxIT/go2rtc/issues/208
|
||||||
|
base = "rtsp://rtsp://turret2-cam.lan:554/stream1/"
|
||||||
|
u, err = urlParse(base)
|
||||||
|
assert.Empty(t, err)
|
||||||
|
assert.Equal(t, "turret2-cam.lan:554", u.Host)
|
||||||
}
|
}
|
||||||
|
@@ -9,7 +9,26 @@ import (
|
|||||||
// Element Producer
|
// Element Producer
|
||||||
|
|
||||||
func (c *Conn) GetMedias() []*streamer.Media {
|
func (c *Conn) GetMedias() []*streamer.Media {
|
||||||
return c.Medias
|
if c.Medias != nil {
|
||||||
|
return c.Medias
|
||||||
|
}
|
||||||
|
|
||||||
|
return []*streamer.Media{
|
||||||
|
{
|
||||||
|
Kind: streamer.KindVideo,
|
||||||
|
Direction: streamer.DirectionRecvonly,
|
||||||
|
Codecs: []*streamer.Codec{
|
||||||
|
{Name: streamer.CodecAll},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Kind: streamer.KindAudio,
|
||||||
|
Direction: streamer.DirectionRecvonly,
|
||||||
|
Codecs: []*streamer.Codec{
|
||||||
|
{Name: streamer.CodecAll},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
|
func (c *Conn) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
|
||||||
@@ -26,7 +45,7 @@ func (c *Conn) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.
|
|||||||
return streamer.NewTrack(codec, media.Direction)
|
return streamer.NewTrack(codec, media.Direction)
|
||||||
}
|
}
|
||||||
|
|
||||||
track, err := c.SetupMedia(media, codec)
|
track, err := c.SetupMedia(media, codec, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -63,11 +82,20 @@ func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.
|
|||||||
codec := track.Codec.Clone()
|
codec := track.Codec.Clone()
|
||||||
codec.PayloadType = uint8(96 + i)
|
codec.PayloadType = uint8(96 + i)
|
||||||
|
|
||||||
for i, m := range c.Medias {
|
if media.MatchAll() {
|
||||||
if m == media {
|
// fill consumer medias list
|
||||||
media.Codecs = []*streamer.Codec{codec}
|
c.Medias = append(c.Medias, &streamer.Media{
|
||||||
c.Medias[i] = media
|
Kind: media.Kind, Direction: media.Direction,
|
||||||
break
|
Codecs: []*streamer.Codec{codec},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// find consumer media and replace codec with right one
|
||||||
|
for i, m := range c.Medias {
|
||||||
|
if m == media {
|
||||||
|
media.Codecs = []*streamer.Codec{codec}
|
||||||
|
c.Medias[i] = media
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -33,9 +33,12 @@ const (
|
|||||||
CodecAAC = "MPEG4-GENERIC"
|
CodecAAC = "MPEG4-GENERIC"
|
||||||
CodecOpus = "OPUS" // payloadType: 111
|
CodecOpus = "OPUS" // payloadType: 111
|
||||||
CodecG722 = "G722"
|
CodecG722 = "G722"
|
||||||
CodecMPA = "MPA" // payload: 14
|
CodecMP3 = "MPA" // payload: 14, aka MPEG-1 Layer III
|
||||||
|
|
||||||
CodecELD = "ELD" // AAC-ELD
|
CodecELD = "ELD" // AAC-ELD
|
||||||
|
|
||||||
|
CodecAll = "ALL"
|
||||||
|
CodecAny = "ANY"
|
||||||
)
|
)
|
||||||
|
|
||||||
const PayloadTypeRAW byte = 255
|
const PayloadTypeRAW byte = 255
|
||||||
@@ -44,7 +47,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, CodecMPA, CodecELD:
|
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecELD:
|
||||||
return KindAudio
|
return KindAudio
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
@@ -60,7 +63,6 @@ type Media struct {
|
|||||||
|
|
||||||
MID string `json:"mid,omitempty"` // TODO: fixme?
|
MID string `json:"mid,omitempty"` // TODO: fixme?
|
||||||
Control string `json:"control,omitempty"` // TODO: fixme?
|
Control string `json:"control,omitempty"` // TODO: fixme?
|
||||||
Title string `json:"title,omitempty"` // TODO: fixme?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Media) String() string {
|
func (m *Media) String() string {
|
||||||
@@ -112,10 +114,6 @@ func (m *Media) MatchMedia(media *Media) *Codec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, localCodec := range m.Codecs {
|
for _, localCodec := range m.Codecs {
|
||||||
if media.Codecs == nil {
|
|
||||||
return localCodec
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, remoteCodec := range media.Codecs {
|
for _, remoteCodec := range media.Codecs {
|
||||||
if localCodec.Match(remoteCodec) {
|
if localCodec.Match(remoteCodec) {
|
||||||
return localCodec
|
return localCodec
|
||||||
@@ -125,6 +123,10 @@ func (m *Media) MatchMedia(media *Media) *Codec {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Media) MatchAll() bool {
|
||||||
|
return len(m.Codecs) > 0 && m.Codecs[0].Name == CodecAll
|
||||||
|
}
|
||||||
|
|
||||||
// Codec take best from:
|
// Codec take best from:
|
||||||
// - deepch/vdk/av.CodecData
|
// - deepch/vdk/av.CodecData
|
||||||
// - pion/webrtc.RTPCodecCapability
|
// - pion/webrtc.RTPCodecCapability
|
||||||
@@ -154,6 +156,11 @@ func (c *Codec) Clone() *Codec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Codec) Match(codec *Codec) bool {
|
func (c *Codec) Match(codec *Codec) bool {
|
||||||
|
switch codec.Name {
|
||||||
|
case CodecAll, CodecAny:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
return c.Name == codec.Name &&
|
return c.Name == codec.Name &&
|
||||||
(c.ClockRate == codec.ClockRate || codec.ClockRate == 0) &&
|
(c.ClockRate == codec.ClockRate || codec.ClockRate == 0) &&
|
||||||
(c.Channels == codec.Channels || codec.Channels == 0)
|
(c.Channels == codec.Channels || codec.Channels == 0)
|
||||||
@@ -286,7 +293,7 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
|
|||||||
c.Name = CodecPCMA
|
c.Name = CodecPCMA
|
||||||
c.ClockRate = 8000
|
c.ClockRate = 8000
|
||||||
case "14":
|
case "14":
|
||||||
c.Name = CodecMPA
|
c.Name = CodecMP3
|
||||||
c.ClockRate = 44100
|
c.ClockRate = 44100
|
||||||
case "26":
|
case "26":
|
||||||
c.Name = CodecJPEG
|
c.Name = CodecJPEG
|
||||||
@@ -299,6 +306,40 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ParseQuery(query map[string][]string) (medias []*Media) {
|
||||||
|
// set media candidates from query list
|
||||||
|
for key, values := range query {
|
||||||
|
switch key {
|
||||||
|
case KindVideo, KindAudio:
|
||||||
|
for _, value := range values {
|
||||||
|
media := &Media{Kind: key, Direction: DirectionRecvonly}
|
||||||
|
|
||||||
|
for _, name := range strings.Split(value, ",") {
|
||||||
|
name = strings.ToUpper(name)
|
||||||
|
|
||||||
|
// check aliases
|
||||||
|
switch name {
|
||||||
|
case "", "COPY":
|
||||||
|
name = CodecAny
|
||||||
|
case "MJPEG":
|
||||||
|
name = CodecJPEG
|
||||||
|
case "AAC":
|
||||||
|
name = CodecAAC
|
||||||
|
case "MP3":
|
||||||
|
name = CodecMP3
|
||||||
|
}
|
||||||
|
|
||||||
|
media.Codecs = append(media.Codecs, &Codec{Name: name})
|
||||||
|
}
|
||||||
|
|
||||||
|
medias = append(medias, media)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func atoi(s string) (i int) {
|
func atoi(s string) (i int) {
|
||||||
i, _ = strconv.Atoi(s)
|
i, _ = strconv.Atoi(s)
|
||||||
return
|
return
|
||||||
|
@@ -3,6 +3,7 @@ package streamer
|
|||||||
import (
|
import (
|
||||||
"github.com/pion/sdp/v3"
|
"github.com/pion/sdp/v3"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,3 +22,21 @@ func TestSDP(t *testing.T) {
|
|||||||
err = sd.Unmarshal(data)
|
err = sd.Unmarshal(data)
|
||||||
assert.Empty(t, err)
|
assert.Empty(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseQuery(t *testing.T) {
|
||||||
|
u, _ := url.Parse("rtsp://localhost:8554/camera1")
|
||||||
|
medias := ParseQuery(u.Query())
|
||||||
|
assert.Nil(t, medias)
|
||||||
|
|
||||||
|
for _, rawULR := range []string{
|
||||||
|
"rtsp://localhost:8554/camera1?video",
|
||||||
|
"rtsp://localhost:8554/camera1?video=copy",
|
||||||
|
"rtsp://localhost:8554/camera1?video=any",
|
||||||
|
} {
|
||||||
|
u, _ = url.Parse(rawULR)
|
||||||
|
medias = ParseQuery(u.Query())
|
||||||
|
assert.Equal(t, []*Media{
|
||||||
|
{Kind: KindVideo, Direction: DirectionRecvonly, Codecs: []*Codec{{Name: CodecAny}}},
|
||||||
|
}, medias)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
200
pkg/ts/ts.go
Normal file
200
pkg/ts/ts.go
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
package ts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
|
"github.com/deepch/vdk/av"
|
||||||
|
"github.com/deepch/vdk/codec/aacparser"
|
||||||
|
"github.com/deepch/vdk/codec/h264parser"
|
||||||
|
"github.com/deepch/vdk/format/ts"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Consumer struct {
|
||||||
|
streamer.Element
|
||||||
|
|
||||||
|
UserAgent string
|
||||||
|
RemoteAddr string
|
||||||
|
|
||||||
|
buf *bytes.Buffer
|
||||||
|
muxer *ts.Muxer
|
||||||
|
mimeType string
|
||||||
|
streams []av.CodecData
|
||||||
|
start bool
|
||||||
|
init []byte
|
||||||
|
|
||||||
|
send uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) GetMedias() []*streamer.Media {
|
||||||
|
return []*streamer.Media{
|
||||||
|
{
|
||||||
|
Kind: streamer.KindVideo,
|
||||||
|
Direction: streamer.DirectionRecvonly,
|
||||||
|
Codecs: []*streamer.Codec{
|
||||||
|
{Name: streamer.CodecH264},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
//{
|
||||||
|
// Kind: streamer.KindAudio,
|
||||||
|
// Direction: streamer.DirectionRecvonly,
|
||||||
|
// Codecs: []*streamer.Codec{
|
||||||
|
// {Name: streamer.CodecAAC},
|
||||||
|
// },
|
||||||
|
//},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||||
|
codec := track.Codec
|
||||||
|
trackID := int8(len(c.streams))
|
||||||
|
|
||||||
|
switch codec.Name {
|
||||||
|
case streamer.CodecH264:
|
||||||
|
sps, pps := h264.GetParameterSet(codec.FmtpLine)
|
||||||
|
stream, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.mimeType) > 0 {
|
||||||
|
c.mimeType += ","
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: fixme
|
||||||
|
// some devices won't play high level
|
||||||
|
if stream.RecordInfo.AVCLevelIndication <= 0x29 {
|
||||||
|
c.mimeType += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
|
||||||
|
} else {
|
||||||
|
c.mimeType += "avc1.640029"
|
||||||
|
}
|
||||||
|
|
||||||
|
c.streams = append(c.streams, stream)
|
||||||
|
|
||||||
|
pkt := av.Packet{Idx: trackID, CompositionTime: time.Millisecond}
|
||||||
|
|
||||||
|
ts2time := time.Second / time.Duration(codec.ClockRate)
|
||||||
|
|
||||||
|
push := func(packet *rtp.Packet) error {
|
||||||
|
if packet.Version != h264.RTPPacketVersionAVC {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.start {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pkt.Data = packet.Payload
|
||||||
|
newTime := time.Duration(packet.Timestamp) * ts2time
|
||||||
|
if pkt.Time > 0 {
|
||||||
|
pkt.Duration = newTime - pkt.Time
|
||||||
|
}
|
||||||
|
pkt.Time = newTime
|
||||||
|
|
||||||
|
if err = c.muxer.WritePacket(pkt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// clone bytes from buffer, so next packet won't overwrite it
|
||||||
|
buf := append([]byte{}, c.buf.Bytes()...)
|
||||||
|
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||||
|
c.Fire(buf)
|
||||||
|
|
||||||
|
c.buf.Reset()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if codec.IsRTP() {
|
||||||
|
wrapper := h264.RTPDepay(track)
|
||||||
|
push = wrapper(push)
|
||||||
|
}
|
||||||
|
|
||||||
|
return track.Bind(push)
|
||||||
|
|
||||||
|
case streamer.CodecAAC:
|
||||||
|
s := streamer.Between(codec.FmtpLine, "config=", ";")
|
||||||
|
|
||||||
|
b, err := hex.DecodeString(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
stream, err := aacparser.NewCodecDataFromMPEG4AudioConfigBytes(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.mimeType) > 0 {
|
||||||
|
c.mimeType += ","
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mimeType += "mp4a.40.2"
|
||||||
|
c.streams = append(c.streams, stream)
|
||||||
|
|
||||||
|
pkt := av.Packet{Idx: trackID, CompositionTime: time.Millisecond}
|
||||||
|
|
||||||
|
ts2time := time.Second / time.Duration(codec.ClockRate)
|
||||||
|
|
||||||
|
push := func(packet *rtp.Packet) error {
|
||||||
|
if !c.start {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pkt.Data = packet.Payload
|
||||||
|
newTime := time.Duration(packet.Timestamp) * ts2time
|
||||||
|
if pkt.Time > 0 {
|
||||||
|
pkt.Duration = newTime - pkt.Time
|
||||||
|
}
|
||||||
|
pkt.Time = newTime
|
||||||
|
|
||||||
|
if err := c.muxer.WritePacket(pkt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// clone bytes from buffer, so next packet won't overwrite it
|
||||||
|
buf := append([]byte{}, c.buf.Bytes()...)
|
||||||
|
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||||
|
c.Fire(buf)
|
||||||
|
|
||||||
|
c.buf.Reset()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if codec.IsRTP() {
|
||||||
|
wrapper := aac.RTPDepay(track)
|
||||||
|
push = wrapper(push)
|
||||||
|
}
|
||||||
|
|
||||||
|
return track.Bind(push)
|
||||||
|
}
|
||||||
|
|
||||||
|
panic("unsupported codec")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) MimeCodecs() string {
|
||||||
|
return c.mimeType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) Init() ([]byte, error) {
|
||||||
|
c.buf = bytes.NewBuffer(nil)
|
||||||
|
c.muxer = ts.NewMuxer(c.buf)
|
||||||
|
|
||||||
|
// first packet will be with header, it's ok
|
||||||
|
if err := c.muxer.WriteHeader(c.streams); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data := append([]byte{}, c.buf.Bytes()...)
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) Start() {
|
||||||
|
c.start = true
|
||||||
|
}
|
@@ -56,3 +56,5 @@ pc.ontrack = ev => {
|
|||||||
- https://web.dev/i18n/en/fast-playback-with-preload/#manual_buffering
|
- https://web.dev/i18n/en/fast-playback-with-preload/#manual_buffering
|
||||||
- https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API
|
- https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API
|
||||||
- https://chromium.googlesource.com/external/w3c/web-platform-tests/+/refs/heads/master/media-source/mediasource-is-type-supported.html
|
- https://chromium.googlesource.com/external/w3c/web-platform-tests/+/refs/heads/master/media-source/mediasource-is-type-supported.html
|
||||||
|
- https://googlechrome.github.io/samples/media/sourcebuffer-changetype.html
|
||||||
|
- https://chromestatus.com/feature/5100845653819392
|
||||||
|
@@ -53,7 +53,7 @@
|
|||||||
const video = document.createElement("video");
|
const video = document.createElement("video");
|
||||||
out.innerText += "video.canPlayType\n";
|
out.innerText += "video.canPlayType\n";
|
||||||
types.forEach(type => {
|
types.forEach(type => {
|
||||||
out.innerText += type + "=" + (video.canPlayType(type) ? "true" : "false") + "\n";
|
out.innerText += `${type} = ${MediaSource.isTypeSupported(type)} / ${video.canPlayType(type)}\n`;
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@@ -89,9 +89,7 @@
|
|||||||
const templates = [
|
const templates = [
|
||||||
'<a href="stream.html?src={name}">stream</a>',
|
'<a href="stream.html?src={name}">stream</a>',
|
||||||
'<a href="webrtc.html?src={name}">2-way-aud</a>',
|
'<a href="webrtc.html?src={name}">2-way-aud</a>',
|
||||||
'<a href="api/stream.mp4?src={name}">mp4</a>',
|
'<a href="links.html?src={name}">links</a>',
|
||||||
'<a href="api/stream.mjpeg?src={name}">mjpeg</a>',
|
|
||||||
'<a href="api/streams?src={name}">info</a>',
|
|
||||||
'<a href="#" data-name="{name}">delete</a>',
|
'<a href="#" data-name="{name}">delete</a>',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -138,15 +136,17 @@
|
|||||||
|
|
||||||
for (const [name, value] of Object.entries(data)) {
|
for (const [name, value] of Object.entries(data)) {
|
||||||
const online = value && value.consumers ? value.consumers.length : 0;
|
const online = value && value.consumers ? value.consumers.length : 0;
|
||||||
|
const src = encodeURIComponent(name);
|
||||||
const links = templates.map(link => {
|
const links = templates.map(link => {
|
||||||
return link.replace("{name}", encodeURIComponent(name));
|
return link.replace("{name}", src);
|
||||||
}).join(" ");
|
}).join(" ");
|
||||||
|
|
||||||
const tr = document.createElement("tr");
|
const tr = document.createElement("tr");
|
||||||
tr.dataset["id"] = name;
|
tr.dataset["id"] = name;
|
||||||
tr.innerHTML =
|
tr.innerHTML =
|
||||||
`<td><label><input type="checkbox" name="${name}">${name}</label></td>` +
|
`<td><label><input type="checkbox" name="${name}">${name}</label></td>` +
|
||||||
`<td>${online}</td><td>${links}</td>`;
|
`<td><a href="api/streams?src=${src}">${online} / info</a></td>` +
|
||||||
|
`<td>${links}</td>`;
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -156,17 +156,9 @@
|
|||||||
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {
|
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {
|
||||||
const info = document.querySelector(".info");
|
const info = document.querySelector(".info");
|
||||||
info.innerText = `Version: ${data.version}, Config: ${data.config_path}`;
|
info.innerText = `Version: ${data.version}, Config: ${data.config_path}`;
|
||||||
|
|
||||||
try {
|
|
||||||
const host = data.host.match(/^[^:]+/)[0];
|
|
||||||
const port = data.rtsp.listen.match(/[0-9]+$/)[0];
|
|
||||||
templates.splice(4, 0, `<a href="rtsp://${host}:${port}/{name}">rtsp</a>`);
|
|
||||||
} catch (e) {
|
|
||||||
templates.splice(4, 0, `<a href="rtsp://${location.host}:8554/{name}">rtsp</a>`);
|
|
||||||
}
|
|
||||||
|
|
||||||
reload();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
reload();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
89
www/links.html
Normal file
89
www/links.html
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>go2rtc - links</title>
|
||||||
|
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body, #config {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div > li {
|
||||||
|
list-style-type: none;
|
||||||
|
padding-left: 10px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
div > li:before {
|
||||||
|
content: "-";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="main.js"></script>
|
||||||
|
<div id="links"></div>
|
||||||
|
<script>
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
const src = params.get("src");
|
||||||
|
|
||||||
|
const links = document.querySelector("#links");
|
||||||
|
links.innerHTML = `
|
||||||
|
<h2>Any codec in source</h2>
|
||||||
|
<li><a href="stream.html?src=${src}">stream.html</a> with auto-select mode / browsers: all / codecs: H264, H265*, MJPEG, JPEG, AAC, PCMU, PCMA, OPUS</li>
|
||||||
|
<li><a href="api/streams?src=${src}">info.json</a> page with active connections</li>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const url = new URL('api', location.href);
|
||||||
|
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {
|
||||||
|
let rtsp = location.host + ':8554';
|
||||||
|
try {
|
||||||
|
const host = data.host.match(/^[^:]+/)[0];
|
||||||
|
const port = data.rtsp.listen.match(/[0-9]+$/)[0];
|
||||||
|
rtsp = `${host}:${port}`;
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
|
||||||
|
const links = document.querySelector("#links");
|
||||||
|
links.innerHTML += `
|
||||||
|
<li><a href="rtsp://${rtsp}/${src}">rtsp</a> with only one video and one audio / codecs: any</li>
|
||||||
|
<li><a href="rtsp://${rtsp}/${src}?mp4">rtsp</a> for MP4 recording (Hass or Frigate) / codecs: H264, H265, AAC</li>
|
||||||
|
<li><a href="rtsp://${rtsp}/${src}?video=all&audio=all">rtsp</a> with all tracks / codecs: any</li>
|
||||||
|
|
||||||
|
<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=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="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}&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/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}&mp4">stream.m3u8</a> HLS/fMP4 / browsers: Safari all, Chrome Android / codecs: H264, H265*, AAC</li>
|
||||||
|
<li><a href="webrtc.html?src=${src}">webrtc.html</a> with two-way audio for supported cameras / browsers: all / codecs: H264, PCMU, PCMA, OPUS</li>
|
||||||
|
|
||||||
|
<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="api/stream.mjpeg?src=${src}">stream.mjpeg</a> MJPEG stream / browsers: all / codecs: MJPEG, JPEG</li>
|
||||||
|
<li><a href="api/frame.jpeg?src=${src}">frame.jpeg</a> snapshot in JPEG-format / browsers: all / codecs: MJPEG, JPEG</li>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@@ -26,8 +26,7 @@ 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
|
||||||
"mp4a.69", // MP3
|
"opus", // OPUS Chrome
|
||||||
"mp4a.6B", // MP3
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Reference in New Issue
Block a user