mirror of
				https://github.com/AlexxIT/go2rtc.git
				synced 2025-10-31 19:53:02 +08:00 
			
		
		
		
	Compare commits
	
		
			35 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 | ||
|   | 3acea1ed5a | ||
|   | 3fb8d9af66 | ||
|   | 9bbaf41d54 | ||
|   | c43530fbd3 | ||
|   | 15777a3d94 | ||
|   | 6e61ac6d2f | ||
|   | 6d7d5f53d8 | ||
|   | d2bca8d461 | ||
|   | 94b089d1e3 | ||
|   | b3d16c9fcc | ||
|   | f0def68482 | 
							
								
								
									
										6
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -2,9 +2,9 @@ name: release | ||||
|  | ||||
| on: | ||||
|   workflow_dispatch: | ||||
|   push: | ||||
|     tags: | ||||
|       - 'v*' | ||||
| #  push: | ||||
| #    tags: | ||||
| #      - 'v*' | ||||
|  | ||||
| jobs: | ||||
|   build-and-release: | ||||
|   | ||||
							
								
								
									
										55
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										55
									
								
								README.md
									
									
									
									
									
								
							| @@ -27,6 +27,40 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg | ||||
| - [MediaSoup](https://mediasoup.org/) framework routing idea | ||||
| - HomeKit Accessory Protocol from [@brutella](https://github.com/brutella/hap) | ||||
|  | ||||
| --- | ||||
|  | ||||
| * [Fast start](#fast-start) | ||||
|   * [go2rtc: Binary](#go2rtc-binary) | ||||
|   * [go2rtc: Home Assistant Add-on](#go2rtc-home-assistant-add-on) | ||||
|   * [go2rtc: Docker](#go2rtc-docker) | ||||
| * [Configuration](#configuration) | ||||
|   * [Module: Streams](#module-streams) | ||||
|     * [Source: RTSP](#source-rtsp) | ||||
|     * [Source: RTMP](#source-rtmp) | ||||
|     * [Source: HTTP](#source-http) | ||||
|     * [Source: FFmpeg](#source-ffmpeg) | ||||
|     * [Source: FFmpeg Device](#source-ffmpeg-device) | ||||
|     * [Source: Exec](#source-exec) | ||||
|     * [Source: Echo](#source-echo) | ||||
|     * [Source: HomeKit](#source-homekit) | ||||
|     * [Source: Ivideon](#source-ivideon) | ||||
|     * [Source: Hass](#source-hass) | ||||
|   * [Module: API](#module-api) | ||||
|   * [Module: RTSP](#module-rtsp) | ||||
|   * [Module: WebRTC](#module-webrtc) | ||||
|   * [Module: Ngrok](#module-ngrok) | ||||
|   * [Module: Hass](#module-hass) | ||||
|     * [From go2rtc to Hass](#from-go2rtc-to-hass) | ||||
|     * [From Hass to go2rtc](#from-hass-to-go2rtc) | ||||
|   * [Module: MP4](#module-mp4) | ||||
|   * [Module: MJPEG](#module-mjpeg) | ||||
|   * [Module: Log](#module-log) | ||||
| * [Security](#security) | ||||
| * [Codecs madness](#codecs-madness) | ||||
| * [Codecs negotiation](#codecs-negotiation) | ||||
| * [TIPS](#tips) | ||||
| * [FAQ](#faq) | ||||
|  | ||||
| ## Fast start | ||||
|  | ||||
| 1. Download [binary](#go2rtc-binary) or use [Docker](#go2rtc-docker) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on) | ||||
| @@ -36,7 +70,6 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg | ||||
|  | ||||
| - add your [streams](#module-streams) to [config](#configuration) file | ||||
| - setup [external access](#module-webrtc) to webrtc | ||||
| - setup [external access](#module-ngrok) to web interface | ||||
|  | ||||
| **Developers:** | ||||
|  | ||||
| @@ -74,14 +107,14 @@ Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support | ||||
|  | ||||
| ## Configuration | ||||
|  | ||||
| Create file `go2rtc.yaml`. go2rtc will search this file in current work dirrectory by default. | ||||
|  | ||||
| - by default, you need to config only your `streams` links | ||||
| - by default go2rtc will search `go2rtc.yaml` in the current work dirrectory | ||||
| - `api` server will start on default **1984 port** (TCP) | ||||
| - `rtsp` server will start on default **8554 port** (TCP) | ||||
| - `webrtc` will use port **8555** (TCP/UDP) for connections | ||||
| - `ffmpeg` will use default transcoding options | ||||
|  | ||||
| Configuration options and a complete list of settings can be found in [the wiki](https://github.com/AlexxIT/go2rtc/wiki/Configuration). | ||||
|  | ||||
| Available modules: | ||||
|  | ||||
| - [streams](#module-streams) | ||||
| @@ -95,8 +128,6 @@ Available modules: | ||||
| - [hass](#module-hass) - Home Assistant integration | ||||
| - [log](#module-log) - logs config | ||||
|  | ||||
| Full default config [example](https://github.com/AlexxIT/go2rtc/wiki/Configuration). | ||||
|  | ||||
| ### Module: Streams | ||||
|  | ||||
| **go2rtc** support different stream source types. You can config one or multiple links of any type as stream source. | ||||
| @@ -359,6 +390,8 @@ go2rtc has simple HTML page (`stream.html`) with support params in URL: | ||||
| ```yaml | ||||
| api: | ||||
|   listen: ":1984"    # default ":1984", HTTP API port ("" - disabled) | ||||
|   username: "admin"  # default "", Basic auth for WebUI | ||||
|   password: "pass"   # default "", Basic auth for WebUI | ||||
|   base_path: "/rtc"  # default "", API prefix for serve on suburl (/api => /rtc/api) | ||||
|   static_dir: "www"  # default "", folder for static files (custom web interface) | ||||
|   origin: "*"        # default "", allow CORS requests (only * supported) | ||||
| @@ -366,7 +399,7 @@ api: | ||||
|  | ||||
| **PS:** | ||||
|  | ||||
| - go2rtc doesn't provide HTTPS or password protection. Use [Nginx](https://nginx.org/) or [Ngrok](#module-ngrok) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on) for this tasks | ||||
| - go2rtc doesn't provide HTTPS. Use [Nginx](https://nginx.org/) or [Ngrok](#module-ngrok) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on) for this tasks | ||||
| - you can access microphone (for 2-way audio) only with HTTPS ([read more](https://stackoverflow.com/questions/52759992/how-to-access-camera-and-microphone-in-chrome-without-https)) | ||||
| - MJPEG over WebSocket plays better than native MJPEG because Chrome [bug](https://bugs.chromium.org/p/chromium/issues/detail?id=527446) | ||||
| - MP4 over WebSocket was created only for Apple iOS because it doesn't support MSE and native MP4 | ||||
| @@ -384,9 +417,9 @@ Password protection always disabled for localhost calls (ex. FFmpeg or Hass on s | ||||
|  | ||||
| ```yaml | ||||
| rtsp: | ||||
|   listen: ":8554"  # RTSP Server TCP port, default - 8554 | ||||
|   username: admin  # optional, default - disabled | ||||
|   password: pass   # optional, default - disabled | ||||
|   listen: ":8554"    # RTSP Server TCP port, default - 8554 | ||||
|   username: "admin"  # optional, default - disabled | ||||
|   password: "pass"   # optional, default - disabled | ||||
| ``` | ||||
|  | ||||
| ### Module: WebRTC | ||||
| @@ -399,7 +432,7 @@ WebRTC usually works without problems in the local network. But external access | ||||
|  | ||||
| ```yaml | ||||
| webrtc: | ||||
|   listen: ":8555" # address of your local server and port (TCP/UDP) | ||||
|   listen: ":8555"  # address of your local server and port (TCP/UDP) | ||||
| ``` | ||||
|  | ||||
| **Static public IP** | ||||
|   | ||||
| @@ -81,7 +81,9 @@ func apiWS(w http.ResponseWriter, r *http.Request) { | ||||
| 	for { | ||||
| 		msg := new(Message) | ||||
| 		if err = ws.ReadJSON(msg); err != nil { | ||||
| 			log.Trace().Err(err).Caller().Send() | ||||
| 			if !websocket.IsCloseError(err, websocket.CloseNoStatusReceived) { | ||||
| 				log.Trace().Err(err).Caller().Send() | ||||
| 			} | ||||
| 			_ = ws.Close() | ||||
| 			break | ||||
| 		} | ||||
|   | ||||
| @@ -14,7 +14,7 @@ import ( | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| var Version = "1.0.0" | ||||
| var Version = "1.1.0" | ||||
| var UserAgent = "go2rtc/" + Version | ||||
|  | ||||
| var ConfigPath string | ||||
|   | ||||
| @@ -15,11 +15,11 @@ func deviceInputSuffix(videoIdx, audioIdx int) string { | ||||
| 	audio := findMedia(streamer.KindAudio, audioIdx) | ||||
| 	switch { | ||||
| 	case video != nil && audio != nil: | ||||
| 		return `"` + video.Title + `:` + audio.Title + `"` | ||||
| 		return `"` + video.MID + `:` + audio.MID + `"` | ||||
| 	case video != nil: | ||||
| 		return `"` + video.Title + `"` | ||||
| 		return `"` + video.MID + `"` | ||||
| 	case audio != nil: | ||||
| 		return `"` + audio.Title + `"` | ||||
| 		return `"` + audio.MID + `"` | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
| @@ -57,7 +57,5 @@ process: | ||||
| } | ||||
|  | ||||
| func loadMedia(kind, name string) *streamer.Media { | ||||
| 	return &streamer.Media{ | ||||
| 		Kind: kind, Title: name, | ||||
| 	} | ||||
| 	return &streamer.Media{Kind: kind, MID: name} | ||||
| } | ||||
|   | ||||
| @@ -13,7 +13,7 @@ const deviceInputPrefix = "-f v4l2" | ||||
|  | ||||
| func deviceInputSuffix(videoIdx, audioIdx int) string { | ||||
| 	video := findMedia(streamer.KindVideo, videoIdx) | ||||
| 	return video.Title | ||||
| 	return video.MID | ||||
| } | ||||
|  | ||||
| func loadMedias() { | ||||
| @@ -44,7 +44,5 @@ func loadMedia(kind, name string) *streamer.Media { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	return &streamer.Media{ | ||||
| 		Kind: kind, Title: name, | ||||
| 	} | ||||
| 	return &streamer.Media{Kind: kind, MID: name} | ||||
| } | ||||
|   | ||||
| @@ -15,11 +15,11 @@ func deviceInputSuffix(videoIdx, audioIdx int) string { | ||||
| 	audio := findMedia(streamer.KindAudio, audioIdx) | ||||
| 	switch { | ||||
| 	case video != nil && audio != nil: | ||||
| 		return `video="` + video.Title + `":audio=` + audio.Title + `"` | ||||
| 		return `video="` + video.MID + `":audio=` + audio.MID + `"` | ||||
| 	case video != nil: | ||||
| 		return `video="` + video.Title + `"` | ||||
| 		return `video="` + video.MID + `"` | ||||
| 	case audio != nil: | ||||
| 		return `audio="` + audio.Title + `"` | ||||
| 		return `audio="` + audio.MID + `"` | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
| @@ -53,7 +53,5 @@ func loadMedias() { | ||||
| } | ||||
|  | ||||
| func loadMedia(kind, name string) *streamer.Media { | ||||
| 	return &streamer.Media{ | ||||
| 		Kind: kind, Title: name, | ||||
| 	} | ||||
| 	return &streamer.Media{Kind: kind, MID: name} | ||||
| } | ||||
|   | ||||
| @@ -67,6 +67,7 @@ var defaults = map[string]string{ | ||||
| 	"pcma/48000": "-c:a pcm_alaw -ar:a 48000 -ac:a 1", | ||||
| 	"aac":        "-c:a aac", // keep sample rate and channels | ||||
| 	"aac/16000":  "-c:a aac -ar:a 16000 -ac:a 1", | ||||
| 	"mp3":        "-c:a libmp3lame -q:a 8", | ||||
|  | ||||
| 	// hardware Intel and AMD on Linux | ||||
| 	// better not to set `-async_depth:v 1` like for QSV, because framedrops | ||||
| @@ -141,6 +142,8 @@ func parseArgs(s string) *Args { | ||||
| 			s += "?video" | ||||
| 		case args.audio > 0 && args.video == 0: | ||||
| 			s += "?audio" | ||||
| 		default: | ||||
| 			s += "?video&audio" | ||||
| 		} | ||||
| 		args.input = strings.Replace(defaults["rtsp"], "{input}", s, 1) | ||||
| 	} 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/streams" | ||||
| 	"github.com/AlexxIT/go2rtc/pkg/mp4" | ||||
| 	"github.com/AlexxIT/go2rtc/pkg/streamer" | ||||
| 	"github.com/rs/zerolog" | ||||
| 	"net/http" | ||||
| 	"strconv" | ||||
| @@ -25,8 +26,14 @@ func Init() { | ||||
| var log zerolog.Logger | ||||
|  | ||||
| func handlerKeyframe(w http.ResponseWriter, r *http.Request) { | ||||
| 	if isChromeFirst(w, r) { | ||||
| 		return | ||||
| 	// Chrome 105 does two requests: without Range and with `Range: bytes=0-` | ||||
| 	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") | ||||
| @@ -67,7 +74,22 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) { | ||||
| func handlerMP4(w http.ResponseWriter, r *http.Request) { | ||||
| 	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 | ||||
| 	} | ||||
|  | ||||
| @@ -83,7 +105,9 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) { | ||||
| 	cons := &mp4.Consumer{ | ||||
| 		RemoteAddr: r.RemoteAddr, | ||||
| 		UserAgent:  r.UserAgent(), | ||||
| 		Medias:     streamer.ParseQuery(r.URL.Query()), | ||||
| 	} | ||||
|  | ||||
| 	cons.Listen(func(msg interface{}) { | ||||
| 		if data, ok := msg.([]byte); ok { | ||||
| 			if _, err := w.Write(data); err != nil && exit != nil { | ||||
| @@ -135,23 +159,3 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) { | ||||
| 		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, | ||||
| 		UserAgent:  tr.Request.UserAgent(), | ||||
| 	} | ||||
| 	cons.UserAgent = tr.Request.UserAgent() | ||||
| 	cons.RemoteAddr = tr.Request.RemoteAddr | ||||
|  | ||||
| 	if codecs, ok := msg.Value.(string); ok { | ||||
| 		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, ",") { | ||||
| 		switch name { | ||||
| 		case "avc1.640029": | ||||
| 		case mp4.MimeH264: | ||||
| 			codec := &streamer.Codec{Name: streamer.CodecH264} | ||||
| 			videos = append(videos, codec) | ||||
| 		case "hvc1.1.6.L153.B0": | ||||
| 		case mp4.MimeH265: | ||||
| 			codec := &streamer.Codec{Name: streamer.CodecH265} | ||||
| 			videos = append(videos, codec) | ||||
| 		case "mp4a.40.2": | ||||
| 		case mp4.MimeAAC: | ||||
| 			codec := &streamer.Codec{Name: streamer.CodecAAC} | ||||
| 			audios = append(audios, codec) | ||||
| 		case mp4.MimeOpus: | ||||
| 			codec := &streamer.Codec{Name: streamer.CodecOpus} | ||||
| 			audios = append(audios, codec) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -3,25 +3,29 @@ package rtsp | ||||
| import ( | ||||
| 	"github.com/AlexxIT/go2rtc/cmd/app" | ||||
| 	"github.com/AlexxIT/go2rtc/cmd/streams" | ||||
| 	"github.com/AlexxIT/go2rtc/pkg/mp4" | ||||
| 	"github.com/AlexxIT/go2rtc/pkg/rtsp" | ||||
| 	"github.com/AlexxIT/go2rtc/pkg/streamer" | ||||
| 	"github.com/AlexxIT/go2rtc/pkg/tcp" | ||||
| 	"github.com/rs/zerolog" | ||||
| 	"net" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| func Init() { | ||||
| 	var conf struct { | ||||
| 		Mod struct { | ||||
| 			Listen   string `yaml:"listen" json:"listen"` | ||||
| 			Username string `yaml:"username" json:"-"` | ||||
| 			Password string `yaml:"password" json:"-"` | ||||
| 			Listen       string `yaml:"listen" json:"listen"` | ||||
| 			Username     string `yaml:"username" json:"-"` | ||||
| 			Password     string `yaml:"password" json:"-"` | ||||
| 			DefaultQuery string `yaml:"default_query"` | ||||
| 		} `yaml:"rtsp"` | ||||
| 	} | ||||
|  | ||||
| 	// default config | ||||
| 	conf.Mod.Listen = ":8554" | ||||
| 	conf.Mod.DefaultQuery = "video&audio" | ||||
|  | ||||
| 	app.LoadConfig(&conf) | ||||
| 	app.Info["rtsp"] = conf.Mod | ||||
| @@ -49,6 +53,10 @@ func Init() { | ||||
|  | ||||
| 	log.Info().Str("addr", address).Msg("[rtsp] listen") | ||||
|  | ||||
| 	if query, err := url.ParseQuery(conf.Mod.DefaultQuery); err == nil { | ||||
| 		defaultMedias = mp4.ParseQuery(query) | ||||
| 	} | ||||
|  | ||||
| 	go func() { | ||||
| 		for { | ||||
| 			conn, err := ln.Accept() | ||||
| @@ -78,6 +86,7 @@ var Port string | ||||
|  | ||||
| var log zerolog.Logger | ||||
| var handlers []Handler | ||||
| var defaultMedias []*streamer.Media | ||||
|  | ||||
| func rtspHandler(url string) (streamer.Producer, error) { | ||||
| 	backchannel := true | ||||
| @@ -164,7 +173,10 @@ func tcpHandler(conn *rtsp.Conn) { | ||||
|  | ||||
| 			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 { | ||||
| 				log.Warn().Err(err).Str("stream", name).Msg("[rtsp]") | ||||
| @@ -228,45 +240,3 @@ func tcpHandler(conn *rtsp.Conn) { | ||||
|  | ||||
| 	_ = 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) | ||||
| 					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/ffmpeg" | ||||
| 	"github.com/AlexxIT/go2rtc/cmd/hass" | ||||
| 	"github.com/AlexxIT/go2rtc/cmd/hls" | ||||
| 	"github.com/AlexxIT/go2rtc/cmd/homekit" | ||||
| 	"github.com/AlexxIT/go2rtc/cmd/http" | ||||
| 	"github.com/AlexxIT/go2rtc/cmd/ivideon" | ||||
| @@ -26,27 +27,25 @@ import ( | ||||
|  | ||||
| func main() { | ||||
| 	app.Init()     // init config and logs | ||||
| 	api.Init()     // init HTTP API server | ||||
| 	streams.Init() // load streams list | ||||
|  | ||||
| 	api.Init() // init HTTP API server | ||||
|  | ||||
| 	echo.Init() | ||||
|  | ||||
| 	rtsp.Init()   // add support RTSP client and RTSP server | ||||
| 	rtmp.Init()   // add support RTMP client | ||||
| 	exec.Init()   // add support exec scheme (depends on RTSP server) | ||||
| 	ffmpeg.Init() // add support ffmpeg scheme (depends on exec scheme) | ||||
| 	hass.Init()   // add support hass scheme | ||||
|  | ||||
| 	webrtc.Init() | ||||
| 	mp4.Init() | ||||
| 	mjpeg.Init() | ||||
| 	http.Init() | ||||
| 	echo.Init() | ||||
| 	ivideon.Init() | ||||
|  | ||||
| 	srtp.Init() | ||||
| 	homekit.Init() | ||||
|  | ||||
| 	ivideon.Init() | ||||
| 	webrtc.Init() | ||||
| 	mp4.Init() | ||||
| 	hls.Init() | ||||
| 	mjpeg.Init() | ||||
| 	http.Init() | ||||
|  | ||||
| 	ngrok.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) | ||||
|  | ||||
| 			data := packet.Payload[2+headersSize:] | ||||
| 			if IsADTS(data) { | ||||
| 				data = data[7:] | ||||
| 			} | ||||
|  | ||||
| 			clone := *packet | ||||
| 			clone.Version = RTPPacketVersionAAC | ||||
| 			clone.Payload = packet.Payload[2+headersSize:] | ||||
| 			clone.Payload = data | ||||
| 			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 | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| package h264 | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/binary" | ||||
| 	"github.com/AlexxIT/go2rtc/pkg/streamer" | ||||
| 	"github.com/pion/rtp" | ||||
| @@ -27,7 +28,8 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc { | ||||
| 			} | ||||
|  | ||||
| 			// Fix TP-Link Tapo TC70: sends SPS and PPS with packet.Marker = true | ||||
| 			if packet.Marker { | ||||
| 			// Reolink Duo 2: sends SPS with Marker and PPS without | ||||
| 			if packet.Marker && len(payload) < 128 { | ||||
| 				switch NALUType(payload) { | ||||
| 				case NALUTypeSPS, NALUTypePPS: | ||||
| 					buf = append(buf, payload...) | ||||
| @@ -68,6 +70,27 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc { | ||||
| 			if len(buf) > 0 { | ||||
| 				payload = append(buf, payload...) | ||||
| 				buf = buf[:0] | ||||
| 			} else { | ||||
| 				// some Chinese buggy cameras has single packet with SPS+PPS+IFrame separated by 00 00 00 01 | ||||
| 				// https://github.com/AlexxIT/WebRTC/issues/391 | ||||
| 				// https://github.com/AlexxIT/WebRTC/issues/392 | ||||
| 				for i := 0; i < len(payload); { | ||||
| 					if i+4 >= len(payload) { | ||||
| 						break | ||||
| 					} | ||||
|  | ||||
| 					size := bytes.Index(payload[i+4:], []byte{0, 0, 0, 1}) | ||||
| 					if size < 0 { | ||||
| 						if i == 0 { | ||||
| 							break | ||||
| 						} | ||||
| 						size = len(payload) - (i + 4) | ||||
| 					} | ||||
|  | ||||
| 					binary.BigEndian.PutUint32(payload[i:], uint32(size)) | ||||
|  | ||||
| 					i += size + 4 | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			//log.Printf("[AVC] %v, len: %d, ts: %10d, seq: %d", Types(payload), len(payload), packet.Timestamp, packet.SequenceNumber) | ||||
|   | ||||
							
								
								
									
										97
									
								
								pkg/httpflv/flvio.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								pkg/httpflv/flvio.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | ||||
| package httpflv | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"github.com/deepch/vdk/format/flv/flvio" | ||||
| 	"github.com/deepch/vdk/utils/bits/pio" | ||||
| 	"io" | ||||
| ) | ||||
|  | ||||
| // TODO: rewrite all of this someday | ||||
|  | ||||
| func ReadTag(r io.Reader, b []byte) (tag flvio.Tag, ts int32, err error) { | ||||
| 	if _, err = io.ReadFull(r, b[:flvio.TagHeaderLength]); err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	var datalen int | ||||
| 	if tag, ts, datalen, err = flvio.ParseTagHeader(b); err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	data := make([]byte, datalen) | ||||
| 	if _, err = io.ReadFull(r, data); err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	n, err := ParseHeader(&tag, data) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	tag.Data = data[n:] | ||||
|  | ||||
| 	if _, err = io.ReadFull(r, b[:4]); err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func ParseHeader(self *flvio.Tag, b []byte) (n int, err error) { | ||||
| 	switch self.Type { | ||||
| 	case flvio.TAG_AUDIO: | ||||
| 		return audioParseHeader(self, b) | ||||
|  | ||||
| 	case flvio.TAG_VIDEO: | ||||
| 		return videoParseHeader(self, b) | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func audioParseHeader(tag *flvio.Tag, b []byte) (n int, err error) { | ||||
| 	if len(b) < n+1 { | ||||
| 		err = fmt.Errorf("audiodata: parse invalid") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	flags := b[n] | ||||
| 	n++ | ||||
| 	tag.SoundFormat = flags >> 4 | ||||
| 	tag.SoundRate = (flags >> 2) & 0x3 | ||||
| 	tag.SoundSize = (flags >> 1) & 0x1 | ||||
| 	tag.SoundType = flags & 0x1 | ||||
|  | ||||
| 	switch tag.SoundFormat { | ||||
| 	case flvio.SOUND_AAC: | ||||
| 		if len(b) < n+1 { | ||||
| 			err = fmt.Errorf("audiodata: parse invalid") | ||||
| 			return | ||||
| 		} | ||||
| 		tag.AACPacketType = b[n] | ||||
| 		n++ | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func videoParseHeader(tag *flvio.Tag, b []byte) (n int, err error) { | ||||
| 	if len(b) < n+1 { | ||||
| 		err = fmt.Errorf("videodata: parse invalid") | ||||
| 		return | ||||
| 	} | ||||
| 	flags := b[n] | ||||
| 	tag.FrameType = flags >> 4 | ||||
| 	tag.CodecID = flags & 0xf | ||||
| 	n++ | ||||
|  | ||||
| 	if len(b) < n+4 { | ||||
| 		err = fmt.Errorf("videodata: parse invalid") | ||||
| 		return | ||||
| 	} | ||||
| 	tag.AVCPacketType = b[n] | ||||
| 	n++ | ||||
|  | ||||
| 	tag.CompositionTime = pio.I24BE(b[n:]) | ||||
| 	n += 3 | ||||
|  | ||||
| 	return | ||||
| } | ||||
| @@ -128,8 +128,10 @@ func (c *Conn) Streams() ([]av.CodecData, error) { | ||||
| 		return []av.CodecData{video, audio}, nil | ||||
| 	} else if video != nil { | ||||
| 		c.videoIdx = 0 | ||||
| 		c.audioIdx = -1 | ||||
| 		return []av.CodecData{video}, nil | ||||
| 	} else if audio != nil { | ||||
| 		c.videoIdx = -1 | ||||
| 		c.audioIdx = 0 | ||||
| 		return []av.CodecData{audio}, nil | ||||
| 	} | ||||
| @@ -139,17 +141,19 @@ func (c *Conn) Streams() ([]av.CodecData, error) { | ||||
|  | ||||
| func (c *Conn) ReadPacket() (av.Packet, error) { | ||||
| 	for { | ||||
| 		tag, ts, err := flvio.ReadTag(c.reader, c.buf) | ||||
| 		tag, ts, err := ReadTag(c.reader, c.buf) | ||||
| 		if err != nil { | ||||
| 			return av.Packet{}, err | ||||
| 		} | ||||
|  | ||||
| 		switch tag.Type { | ||||
| 		case flvio.TAG_VIDEO: | ||||
| 			if tag.AVCPacketType != flvio.AVC_NALU { | ||||
| 			if c.videoIdx < 0 || tag.AVCPacketType != flvio.AVC_NALU { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			//log.Printf("[FLV] %v, len: %d, ts: %10d", h264.Types(tag.Data), len(tag.Data), flvio.TsToTime(ts)) | ||||
|  | ||||
| 			return av.Packet{ | ||||
| 				Idx:             c.videoIdx, | ||||
| 				Data:            tag.Data, | ||||
| @@ -159,7 +163,7 @@ func (c *Conn) ReadPacket() (av.Packet, error) { | ||||
| 			}, nil | ||||
|  | ||||
| 		case flvio.TAG_AUDIO: | ||||
| 			if tag.SoundFormat != flvio.SOUND_AAC || tag.AACPacketType != flvio.AAC_RAW { | ||||
| 			if c.audioIdx < 0 || tag.SoundFormat != flvio.SOUND_AAC || tag.AACPacketType != flvio.AAC_RAW { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
|   | ||||
							
								
								
									
										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 | ||||
|  | ||||
| Browser     | avc1 | hvc1 | hev1 | ||||
| ------------|------|------|--- | ||||
| Mac Chrome  | +    | -    | + | ||||
| Mac Safari  | +    | +    | - | ||||
| iOS 15?     | +    | +    | - | ||||
| Mac Firefox | +    | -    | - | ||||
| iOS 12      | +    | -    | - | ||||
| 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, | ||||
| ``` | ||||
| | Browser     | avc1 | hvc1 | hev1 | | ||||
| |-------------|------|------|------| | ||||
|  | Mac Chrome  | +    | -    | +    | | ||||
|  | Mac Safari  | +    | +    | -    | | ||||
|  | iOS 15?     | +    | +    | -    | | ||||
|  | Mac Firefox | +    | -    | -    | | ||||
|  | iOS 12      | +    | -    | -    | | ||||
|  | Android 13  | +    | -    | -    | | ||||
|  | ||||
| ## Useful links | ||||
|  | ||||
|   | ||||
| @@ -24,6 +24,16 @@ type Consumer struct { | ||||
| 	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 ( | ||||
| 	waitNone byte = iota | ||||
| 	waitKeyframe | ||||
| @@ -140,14 +150,33 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea | ||||
| 			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) | ||||
| 	} | ||||
|  | ||||
| 	panic("unsupported codec") | ||||
| } | ||||
|  | ||||
| func (c *Consumer) MimeCodecs() string { | ||||
| 	return c.muxer.MimeCodecs(c.codecs) | ||||
| } | ||||
|  | ||||
| func (c *Consumer) MimeType() string { | ||||
| 	return c.muxer.MimeType(c.codecs) | ||||
| 	return `video/mp4; codecs="` + c.MimeCodecs() + `"` | ||||
| } | ||||
|  | ||||
| func (c *Consumer) Init() ([]byte, error) { | ||||
|   | ||||
							
								
								
									
										221
									
								
								pkg/mp4/muxer.go
									
									
									
									
									
								
							
							
						
						
									
										221
									
								
								pkg/mp4/muxer.go
									
									
									
									
									
								
							| @@ -1,17 +1,13 @@ | ||||
| package mp4 | ||||
|  | ||||
| import ( | ||||
| 	"encoding/binary" | ||||
| 	"encoding/hex" | ||||
| 	"github.com/AlexxIT/go2rtc/pkg/h264" | ||||
| 	"github.com/AlexxIT/go2rtc/pkg/h265" | ||||
| 	"github.com/AlexxIT/go2rtc/pkg/iso" | ||||
| 	"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" | ||||
| ) | ||||
|  | ||||
| @@ -21,8 +17,15 @@ type Muxer struct { | ||||
| 	pts       []uint32 | ||||
| } | ||||
|  | ||||
| func (m *Muxer) MimeType(codecs []*streamer.Codec) string { | ||||
| 	s := `video/mp4; codecs="` | ||||
| const ( | ||||
| 	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 { | ||||
| 		if i > 0 { | ||||
| @@ -35,17 +38,23 @@ func (m *Muxer) MimeType(codecs []*streamer.Codec) string { | ||||
| 		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" | ||||
| 			s += MimeH265 | ||||
| 		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) { | ||||
| 	moov := MOOV() | ||||
| 	mv := iso.NewMovie(1024) | ||||
| 	mv.WriteFileType() | ||||
|  | ||||
| 	mv.StartAtom(iso.Moov) | ||||
| 	mv.WriteMovieHeader() | ||||
|  | ||||
| 	for i, codec := range codecs { | ||||
| 		switch codec.Name { | ||||
| @@ -62,35 +71,11 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) { | ||||
| 				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) | ||||
| 			mv.WriteVideoTrack( | ||||
| 				uint32(i+1), codec.Name, codec.ClockRate, | ||||
| 				uint16(codecData.Width()), uint16(codecData.Height()), | ||||
| 				codecData.AVCDecoderConfRecordBytes(), | ||||
| 			) | ||||
|  | ||||
| 		case streamer.CodecH265: | ||||
| 			vps, sps, pps := h265.GetParameterSet(codec.FmtpLine) | ||||
| @@ -106,35 +91,11 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) { | ||||
| 				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) | ||||
| 			mv.WriteVideoTrack( | ||||
| 				uint32(i+1), codec.Name, codec.ClockRate, | ||||
| 				uint16(codecData.Width()), uint16(codecData.Height()), | ||||
| 				codecData.AVCDecoderConfRecordBytes(), | ||||
| 			) | ||||
|  | ||||
| 		case streamer.CodecAAC: | ||||
| 			s := streamer.Between(codec.FmtpLine, "config=", ";") | ||||
| @@ -143,44 +104,29 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) { | ||||
| 				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) | ||||
| 			mv.WriteAudioTrack( | ||||
| 				uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, b, | ||||
| 			) | ||||
|  | ||||
| 			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) | ||||
| 		case streamer.CodecOpus, streamer.CodecMP3, streamer.CodecPCMU, streamer.CodecPCMA: | ||||
| 			mv.WriteAudioTrack( | ||||
| 				uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, nil, | ||||
| 			) | ||||
| 		} | ||||
|  | ||||
| 		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) | ||||
| 	mv.StartAtom(iso.MoovMvex) | ||||
| 	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() { | ||||
| @@ -192,65 +138,28 @@ func (m *Muxer) Reset() { | ||||
| } | ||||
|  | ||||
| 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) | ||||
| 	// important before increment | ||||
| 	time := m.dts[trackID] | ||||
|  | ||||
| 	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 | ||||
| 	} | ||||
|  | ||||
| 	c.MimeType = muxer.MimeType(codecs) | ||||
| 	c.MimeType = `video/mp4; codecs="` + muxer.MimeCodecs(codecs) + `"` | ||||
|  | ||||
| 	switch track.Codec.Name { | ||||
| 	case streamer.CodecH264: | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| package mp4f | ||||
| package mp4 | ||||
| 
 | ||||
| import ( | ||||
| 	"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" | ||||
| @@ -14,6 +16,7 @@ import ( | ||||
| type Consumer struct { | ||||
| 	streamer.Element | ||||
| 
 | ||||
| 	Medias     []*streamer.Media | ||||
| 	UserAgent  string | ||||
| 	RemoteAddr string | ||||
| 
 | ||||
| @@ -26,6 +29,10 @@ type Consumer struct { | ||||
| } | ||||
| 
 | ||||
| func (c *Consumer) GetMedias() []*streamer.Media { | ||||
| 	if c.Medias != nil { | ||||
| 		return c.Medias | ||||
| 	} | ||||
| 
 | ||||
| 	return []*streamer.Media{ | ||||
| 		{ | ||||
| 			Kind:      streamer.KindVideo, | ||||
| @@ -96,7 +103,17 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea | ||||
| 		return track.Bind(push) | ||||
| 
 | ||||
| 	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.streams = append(c.streams, stream) | ||||
| @@ -126,6 +143,11 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea | ||||
| 			return nil | ||||
| 		} | ||||
| 
 | ||||
| 		if codec.IsRTP() { | ||||
| 			wrapper := aac.RTPDepay(track) | ||||
| 			push = wrapper(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) | ||||
| } | ||||
| @@ -2,7 +2,6 @@ package rtsp | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"bytes" | ||||
| 	"crypto/tls" | ||||
| 	"encoding/binary" | ||||
| 	"errors" | ||||
| @@ -281,7 +280,7 @@ func (c *Conn) Options() error { | ||||
| 	} | ||||
|  | ||||
| 	if val := res.Header.Get("Content-Base"); val != "" { | ||||
| 		c.URL, err = url.Parse(val) | ||||
| 		c.URL, err = urlParse(val) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| @@ -311,7 +310,7 @@ func (c *Conn) Describe() error { | ||||
| 	} | ||||
|  | ||||
| 	if val := res.Header.Get("Content-Base"); val != "" { | ||||
| 		c.URL, err = url.Parse(val) | ||||
| 		c.URL, err = urlParse(val) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| @@ -348,7 +347,7 @@ func (c *Conn) Describe() error { | ||||
|  | ||||
| func (c *Conn) Setup() error { | ||||
| 	for _, media := range c.Medias { | ||||
| 		_, err := c.SetupMedia(media, media.Codecs[0]) | ||||
| 		_, err := c.SetupMedia(media, media.Codecs[0], true) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| @@ -357,11 +356,12 @@ func (c *Conn) Setup() error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (c *Conn) SetupMedia( | ||||
| 	media *streamer.Media, codec *streamer.Codec, | ||||
| ) (*streamer.Track, error) { | ||||
| 	c.stateMu.Lock() | ||||
| 	defer c.stateMu.Unlock() | ||||
| func (c *Conn) SetupMedia(media *streamer.Media, codec *streamer.Codec, first bool) (*streamer.Track, error) { | ||||
| 	// TODO: rewrite recoonection and first flag | ||||
| 	if first { | ||||
| 		c.stateMu.Lock() | ||||
| 		defer c.stateMu.Unlock() | ||||
| 	} | ||||
|  | ||||
| 	if c.state != StateConn && c.state != StateSetup { | ||||
| 		return nil, fmt.Errorf("RTSP SETUP from wrong state: %s", c.state) | ||||
| @@ -380,7 +380,7 @@ func (c *Conn) SetupMedia( | ||||
| 		} | ||||
| 		rawURL += media.Control | ||||
| 	} | ||||
| 	trackURL, err := url.Parse(rawURL) | ||||
| 	trackURL, err := urlParse(rawURL) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| @@ -413,7 +413,7 @@ func (c *Conn) SetupMedia( | ||||
|  | ||||
| 			for _, newMedia := range c.Medias { | ||||
| 				if newMedia.Control == media.Control { | ||||
| 					return c.SetupMedia(newMedia, newMedia.Codecs[0]) | ||||
| 					return c.SetupMedia(newMedia, newMedia.Codecs[0], false) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| @@ -882,42 +882,3 @@ func (c *Conn) bindTrack( | ||||
|  | ||||
| 	return track.Bind(push) | ||||
| } | ||||
|  | ||||
| type RTCP struct { | ||||
| 	Channel byte | ||||
| 	Header  rtcp.Header | ||||
| 	Packets []rtcp.Packet | ||||
| } | ||||
|  | ||||
| const sdpHeader = `v=0 | ||||
| o=- 0 0 IN IP4 0.0.0.0 | ||||
| s=- | ||||
| t=0 0` | ||||
|  | ||||
| func UnmarshalSDP(rawSDP []byte) ([]*streamer.Media, error) { | ||||
| 	medias, err := streamer.UnmarshalSDP(rawSDP) | ||||
| 	if err != nil { | ||||
| 		// fix SDP header for some cameras | ||||
| 		i := bytes.Index(rawSDP, []byte("\nm=")) | ||||
| 		if i > 0 { | ||||
| 			rawSDP = append([]byte(sdpHeader), rawSDP[i:]...) | ||||
| 			medias, err = streamer.UnmarshalSDP(rawSDP) | ||||
| 		} | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// fix bug in ONVIF spec | ||||
| 	// https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec-v241.pdf | ||||
| 	for _, media := range medias { | ||||
| 		switch media.Direction { | ||||
| 		case streamer.DirectionRecvonly, "": | ||||
| 			media.Direction = streamer.DirectionSendonly | ||||
| 		case streamer.DirectionSendonly: | ||||
| 			media.Direction = streamer.DirectionRecvonly | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return medias, nil | ||||
| } | ||||
|   | ||||
							
								
								
									
										68
									
								
								pkg/rtsp/helpers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								pkg/rtsp/helpers.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| package rtsp | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"github.com/AlexxIT/go2rtc/pkg/streamer" | ||||
| 	"github.com/pion/rtcp" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| type RTCP struct { | ||||
| 	Channel byte | ||||
| 	Header  rtcp.Header | ||||
| 	Packets []rtcp.Packet | ||||
| } | ||||
|  | ||||
| const sdpHeader = `v=0 | ||||
| o=- 0 0 IN IP4 0.0.0.0 | ||||
| s=- | ||||
| t=0 0` | ||||
|  | ||||
| func UnmarshalSDP(rawSDP []byte) ([]*streamer.Media, error) { | ||||
| 	medias, err := streamer.UnmarshalSDP(rawSDP) | ||||
| 	if err != nil { | ||||
| 		// fix SDP header for some cameras | ||||
| 		i := bytes.Index(rawSDP, []byte("\nm=")) | ||||
| 		if i > 0 { | ||||
| 			rawSDP = append([]byte(sdpHeader), rawSDP[i:]...) | ||||
| 			medias, err = streamer.UnmarshalSDP(rawSDP) | ||||
| 		} | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// fix bug in ONVIF spec | ||||
| 	// https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec-v241.pdf | ||||
| 	for _, media := range medias { | ||||
| 		switch media.Direction { | ||||
| 		case streamer.DirectionRecvonly, "": | ||||
| 			media.Direction = streamer.DirectionSendonly | ||||
| 		case streamer.DirectionSendonly: | ||||
| 			media.Direction = streamer.DirectionRecvonly | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return medias, nil | ||||
| } | ||||
|  | ||||
| // urlParse fix bugs: | ||||
| // 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) { | ||||
| 	if strings.HasPrefix(rawURL, "rtsp://rtsp://") { | ||||
| 		rawURL = rawURL[7:] | ||||
| 	} | ||||
|  | ||||
| 	u, err := url.Parse(rawURL) | ||||
| 	if err != nil && strings.HasSuffix(err.Error(), "after host") { | ||||
| 		if i1 := strings.Index(rawURL, "://"); i1 > 0 { | ||||
| 			if i2 := strings.IndexByte(rawURL[i1+3:], '/'); i2 > 0 { | ||||
| 				return urlParse(rawURL[:i1+3+i2] + ":" + rawURL[i1+3+i2:]) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return u, err | ||||
| } | ||||
							
								
								
									
										20
									
								
								pkg/rtsp/rtsp_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								pkg/rtsp/rtsp_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| package rtsp | ||||
|  | ||||
| import ( | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestURLParse(t *testing.T) { | ||||
| 	// https://github.com/AlexxIT/WebRTC/issues/395 | ||||
| 	base := "rtsp://::ffff:192.168.1.123/onvif/profile.1/" | ||||
| 	u, err := urlParse(base) | ||||
| 	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 | ||||
|  | ||||
| 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 { | ||||
| @@ -26,7 +45,7 @@ func (c *Conn) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer. | ||||
| 		return streamer.NewTrack(codec, media.Direction) | ||||
| 	} | ||||
|  | ||||
| 	track, err := c.SetupMedia(media, codec) | ||||
| 	track, err := c.SetupMedia(media, codec, true) | ||||
| 	if err != nil { | ||||
| 		return nil | ||||
| 	} | ||||
| @@ -63,11 +82,20 @@ func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer. | ||||
| 		codec := track.Codec.Clone() | ||||
| 		codec.PayloadType = uint8(96 + i) | ||||
|  | ||||
| 		for i, m := range c.Medias { | ||||
| 			if m == media { | ||||
| 				media.Codecs = []*streamer.Codec{codec} | ||||
| 				c.Medias[i] = media | ||||
| 				break | ||||
| 		if media.MatchAll() { | ||||
| 			// fill consumer medias list | ||||
| 			c.Medias = append(c.Medias, &streamer.Media{ | ||||
| 				Kind: media.Kind, Direction: media.Direction, | ||||
| 				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" | ||||
| 	CodecOpus = "OPUS" // payloadType: 111 | ||||
| 	CodecG722 = "G722" | ||||
| 	CodecMPA  = "MPA" // payload: 14 | ||||
| 	CodecMP3  = "MPA" // payload: 14, aka MPEG-1 Layer III | ||||
|  | ||||
| 	CodecELD = "ELD" // AAC-ELD | ||||
|  | ||||
| 	CodecAll = "ALL" | ||||
| 	CodecAny = "ANY" | ||||
| ) | ||||
|  | ||||
| const PayloadTypeRAW byte = 255 | ||||
| @@ -44,7 +47,7 @@ func GetKind(name string) string { | ||||
| 	switch name { | ||||
| 	case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG: | ||||
| 		return KindVideo | ||||
| 	case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMPA, CodecELD: | ||||
| 	case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecELD: | ||||
| 		return KindAudio | ||||
| 	} | ||||
| 	return "" | ||||
| @@ -60,7 +63,6 @@ type Media struct { | ||||
|  | ||||
| 	MID     string `json:"mid,omitempty"`     // TODO: fixme? | ||||
| 	Control string `json:"control,omitempty"` // TODO: fixme? | ||||
| 	Title   string `json:"title,omitempty"`   // TODO: fixme? | ||||
| } | ||||
|  | ||||
| func (m *Media) String() string { | ||||
| @@ -112,10 +114,6 @@ func (m *Media) MatchMedia(media *Media) *Codec { | ||||
| 	} | ||||
|  | ||||
| 	for _, localCodec := range m.Codecs { | ||||
| 		if media.Codecs == nil { | ||||
| 			return localCodec | ||||
| 		} | ||||
|  | ||||
| 		for _, remoteCodec := range media.Codecs { | ||||
| 			if localCodec.Match(remoteCodec) { | ||||
| 				return localCodec | ||||
| @@ -125,6 +123,10 @@ func (m *Media) MatchMedia(media *Media) *Codec { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (m *Media) MatchAll() bool { | ||||
| 	return len(m.Codecs) > 0 && m.Codecs[0].Name == CodecAll | ||||
| } | ||||
|  | ||||
| // Codec take best from: | ||||
| // - deepch/vdk/av.CodecData | ||||
| // - pion/webrtc.RTPCodecCapability | ||||
| @@ -154,6 +156,11 @@ func (c *Codec) Clone() *Codec { | ||||
| } | ||||
|  | ||||
| func (c *Codec) Match(codec *Codec) bool { | ||||
| 	switch codec.Name { | ||||
| 	case CodecAll, CodecAny: | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	return c.Name == codec.Name && | ||||
| 		(c.ClockRate == codec.ClockRate || codec.ClockRate == 0) && | ||||
| 		(c.Channels == codec.Channels || codec.Channels == 0) | ||||
| @@ -286,7 +293,7 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec { | ||||
| 			c.Name = CodecPCMA | ||||
| 			c.ClockRate = 8000 | ||||
| 		case "14": | ||||
| 			c.Name = CodecMPA | ||||
| 			c.Name = CodecMP3 | ||||
| 			c.ClockRate = 44100 | ||||
| 		case "26": | ||||
| 			c.Name = CodecJPEG | ||||
| @@ -299,6 +306,40 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec { | ||||
| 	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) { | ||||
| 	i, _ = strconv.Atoi(s) | ||||
| 	return | ||||
|   | ||||
| @@ -3,6 +3,7 @@ package streamer | ||||
| import ( | ||||
| 	"github.com/pion/sdp/v3" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"net/url" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| @@ -21,3 +22,21 @@ func TestSDP(t *testing.T) { | ||||
| 	err = sd.Unmarshal(data) | ||||
| 	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://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://googlechrome.github.io/samples/media/sourcebuffer-changetype.html | ||||
| - https://chromestatus.com/feature/5100845653819392 | ||||
|   | ||||
| @@ -53,7 +53,7 @@ | ||||
|     const video = document.createElement("video"); | ||||
|     out.innerText += "video.canPlayType\n"; | ||||
|     types.forEach(type => { | ||||
|         out.innerText += type + "=" + (video.canPlayType(type) ? "true" : "false") + "\n"; | ||||
|         out.innerText += `${type} = ${MediaSource.isTypeSupported(type)} / ${video.canPlayType(type)}\n`; | ||||
|     }) | ||||
|  | ||||
| </script> | ||||
|   | ||||
| @@ -89,9 +89,7 @@ | ||||
|     const templates = [ | ||||
|         '<a href="stream.html?src={name}">stream</a>', | ||||
|         '<a href="webrtc.html?src={name}">2-way-aud</a>', | ||||
|         '<a href="api/stream.mp4?src={name}">mp4</a>', | ||||
|         '<a href="api/stream.mjpeg?src={name}">mjpeg</a>', | ||||
|         '<a href="api/streams?src={name}">info</a>', | ||||
|         '<a href="links.html?src={name}">links</a>', | ||||
|         '<a href="#" data-name="{name}">delete</a>', | ||||
|     ]; | ||||
|  | ||||
| @@ -138,15 +136,17 @@ | ||||
|  | ||||
|             for (const [name, value] of Object.entries(data)) { | ||||
|                 const online = value && value.consumers ? value.consumers.length : 0; | ||||
|                 const src = encodeURIComponent(name); | ||||
|                 const links = templates.map(link => { | ||||
|                     return link.replace("{name}", encodeURIComponent(name)); | ||||
|                     return link.replace("{name}", src); | ||||
|                 }).join(" "); | ||||
|  | ||||
|                 const tr = document.createElement("tr"); | ||||
|                 tr.dataset["id"] = name; | ||||
|                 tr.innerHTML = | ||||
|                     `<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); | ||||
|             } | ||||
|         }); | ||||
| @@ -156,17 +156,9 @@ | ||||
|     fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => { | ||||
|         const info = document.querySelector(".info"); | ||||
|         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> | ||||
| </body> | ||||
| </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) | ||||
|             "mp4a.40.2",        // AAC LC | ||||
|             "mp4a.40.5",        // AAC HE | ||||
|             "mp4a.69",          // MP3 | ||||
|             "mp4a.6B",          // MP3 | ||||
|             "opus",             // OPUS Chrome | ||||
|         ]; | ||||
|  | ||||
|         /** | ||||
| @@ -395,11 +394,11 @@ export class VideoRTC extends HTMLElement { | ||||
|                         bufLen = 0; | ||||
|                         sb.appendBuffer(data); | ||||
|                     } else if (sb.buffered && sb.buffered.length) { | ||||
|                         const end = sb.buffered.end(sb.buffered.length - 1) - 5; | ||||
|                         const end = sb.buffered.end(sb.buffered.length - 1) - 15; | ||||
|                         const start = sb.buffered.start(0); | ||||
|                         if (end > start) { | ||||
|                             sb.remove(start, end); | ||||
|                             ms.setLiveSeekableRange(end, end + 5); | ||||
|                             ms.setLiveSeekableRange(end, end + 15); | ||||
|                         } | ||||
|                         // console.debug("VideoRTC.buffered", start, end); | ||||
|                     } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user