mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2025-10-03 23:46:27 +08:00
Compare commits
60 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6f2af78392 | ||
![]() |
548d8133eb | ||
![]() |
36ee2b29fb | ||
![]() |
05accb4555 | ||
![]() |
f949a278da | ||
![]() |
bfae16f3a0 | ||
![]() |
d09d21434b | ||
![]() |
2b9926cedb | ||
![]() |
af24fd67aa | ||
![]() |
e2cd34ffe3 | ||
![]() |
ecdf5ba271 | ||
![]() |
995ef5bb36 | ||
![]() |
8165adcab1 | ||
![]() |
91c4a3e7b5 | ||
![]() |
cb710ea2be | ||
![]() |
843a3ae9c9 | ||
![]() |
de040fb160 | ||
![]() |
acec8a76aa | ||
![]() |
6c07c59454 | ||
![]() |
4d708b5385 | ||
![]() |
2e9f3181d4 | ||
![]() |
3ae15d8f80 | ||
![]() |
d016529030 | ||
![]() |
09f1553e40 | ||
![]() |
52e4bf1b35 | ||
![]() |
bbe6ae0059 | ||
![]() |
c02117e626 | ||
![]() |
b8fb3acbab | ||
![]() |
d4d0064220 | ||
![]() |
855bbdeb60 | ||
![]() |
05893c9203 | ||
![]() |
c9c8e73587 | ||
![]() |
c7b6eb5d5b | ||
![]() |
96bc88d8ce | ||
![]() |
9a2e9dd6d1 | ||
![]() |
b252fcaaa1 | ||
![]() |
c582b932c7 | ||
![]() |
c3f26c4db8 | ||
![]() |
f27f7d28bb | ||
![]() |
0424b1a92a | ||
![]() |
81fb8fc238 | ||
![]() |
037970a4ea | ||
![]() |
3f6e83e87c | ||
![]() |
aa5b23fa80 | ||
![]() |
02bde2c8b7 | ||
![]() |
cb5e90cc3b | ||
![]() |
209fe09806 | ||
![]() |
dca8279e0c | ||
![]() |
8163c7a520 | ||
![]() |
4dffceaf7e | ||
![]() |
9f1e33e0c6 | ||
![]() |
9a7d7e68e2 | ||
![]() |
ab18d5d1ca | ||
![]() |
6e53e74742 | ||
![]() |
f910bd4fce | ||
![]() |
93e475f3a4 | ||
![]() |
e5d8170037 | ||
![]() |
861632f92b | ||
![]() |
9cf75565b5 | ||
![]() |
9368a6b85e |
59
README.md
59
README.md
@@ -14,6 +14,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
||||
- streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [DVRIP](#source-dvrip), [HTTP](#source-http) (FLV/MJPEG/JPEG/TS), [USB Cameras](#source-ffmpeg-device) and [other sources](#module-streams)
|
||||
- streaming from any sources, supported by [FFmpeg](#source-ffmpeg)
|
||||
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4), [HomeKit](#module-homekit) [HLS](#module-hls) or [MJPEG](#module-mjpeg)
|
||||
- [publish](#publish-stream) any source to popular streaming services (YouTube, Telegram, etc.)
|
||||
- first project in the World with support streaming from [HomeKit Cameras](#source-homekit)
|
||||
- support H265 for WebRTC in browser (Safari only, [read more](https://github.com/AlexxIT/Blog/issues/5))
|
||||
- on the fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg)
|
||||
@@ -67,8 +68,10 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
||||
* [Source: WebTorrent](#source-webtorrent)
|
||||
* [Incoming sources](#incoming-sources)
|
||||
* [Stream to camera](#stream-to-camera)
|
||||
* [Publish stream](#publish-stream)
|
||||
* [Module: API](#module-api)
|
||||
* [Module: RTSP](#module-rtsp)
|
||||
* [Module: RTMP](#module-rtmp)
|
||||
* [Module: WebRTC](#module-webrtc)
|
||||
* [Module: HomeKit](#module-homekit)
|
||||
* [Module: WebTorrent](#module-webtorrent)
|
||||
@@ -202,6 +205,7 @@ Read more about [incoming sources](#incoming-sources)
|
||||
Supported for sources:
|
||||
|
||||
- [RTSP cameras](#source-rtsp) with [ONVIF Profile T](https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec.pdf) (back channel connection)
|
||||
- [DVRIP](#source-dvrip) cameras
|
||||
- [TP-Link Tapo](#source-tapo) cameras
|
||||
- [Hikvision ISAPI](#source-isapi) cameras
|
||||
- [Roborock vacuums](#source-roborock) models with cameras
|
||||
@@ -478,7 +482,11 @@ Other names: DVR-IP, NetSurveillance, Sofia protocol (NETsurveillance ActiveX pl
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
camera1: dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0
|
||||
only_stream: dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0
|
||||
only_tts: dvrip://username:password@192.168.1.123:34567?backchannel=1
|
||||
two_way_audio:
|
||||
- dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0
|
||||
- dvrip://username:password@192.168.1.123:34567?backchannel=1
|
||||
```
|
||||
|
||||
#### Source: Tapo
|
||||
@@ -590,7 +598,7 @@ This source type support four connection formats.
|
||||
|
||||
**whep**
|
||||
|
||||
[WebRTC/WHEP](https://www.ietf.org/id/draft-murillo-whep-01.html) - is an unapproved standard for WebRTC video/audio viewers. But it may already be supported in some third-party software. It is supported in go2rtc.
|
||||
[WebRTC/WHEP](https://www.ietf.org/id/draft-murillo-whep-02.html) - is an unapproved standard for WebRTC video/audio viewers. But it may already be supported in some third-party software. It is supported in go2rtc.
|
||||
|
||||
**go2rtc**
|
||||
|
||||
@@ -632,7 +640,7 @@ streams:
|
||||
|
||||
By default, go2rtc establishes a connection to the source when any client requests it. Go2rtc drops the connection to the source when it has no clients left.
|
||||
|
||||
- Go2rtc also can accepts incoming sources in [RTSP](#source-rtsp), [HTTP](#source-http) and **WebRTC/WHIP** formats
|
||||
- Go2rtc also can accepts incoming sources in [RTSP](#module-rtsp), [RTMP](#module-rtmp), [HTTP](#source-http) and **WebRTC/WHIP** formats
|
||||
- Go2rtc won't stop such a source if it has no clients
|
||||
- You can push data only to existing stream (create stream with empty source in config)
|
||||
- You can push multiple incoming sources to same stream
|
||||
@@ -693,6 +701,39 @@ POST http://localhost:1984/api/streams?dst=camera1&src=ffmpeg:http://example.com
|
||||
- you can stop active playback by calling the API with the empty `src` parameter
|
||||
- you will see one active producer and one active consumer in go2rtc WebUI info page during streaming
|
||||
|
||||
### Publish stream
|
||||
|
||||
You can publish any stream to streaming services (YouTube, Telegram, etc.) via RTMP/RTMPS. Important:
|
||||
|
||||
- Supported codecs: H264 for video and AAC for audio
|
||||
- Pixel format should be `yuv420p`, for cameras with `yuvj420p` format you SHOULD use [transcoding](#source-ffmpeg)
|
||||
- You don't need to enable [RTMP module](#module-rtmp) listening for this task
|
||||
|
||||
You can use API:
|
||||
|
||||
```
|
||||
POST http://localhost:1984/api/streams?src=camera1&dst=rtmps://...
|
||||
```
|
||||
|
||||
Or config file:
|
||||
|
||||
```yaml
|
||||
publish:
|
||||
# publish stream "tplink_tapo" to Telegram
|
||||
tplink_tapo: rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
|
||||
# publish stream "other_camera" to Telegram and YouTube
|
||||
other_camera:
|
||||
- rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
|
||||
- rtmps://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx
|
||||
|
||||
streams:
|
||||
# for TP-Link cameras it's important to use transcoding because of wrong pixel format
|
||||
tplink_tapo: ffmpeg:rtsp://user:pass@192.168.1.123/stream1#video=h264#hardware=vaapi#audio=aac
|
||||
```
|
||||
|
||||
- **Telegram Desktop App** > Any public or private channel or group (where you admin) > Live stream > Start with... > Start streaming.
|
||||
- **YouTube** > Create > Go live > Stream latency: Ultra low-latency > Copy: Stream URL + Stream key.
|
||||
|
||||
### Module: API
|
||||
|
||||
The HTTP API is the main part for interacting with the application. Default address: `http://localhost:1984/`.
|
||||
@@ -705,6 +746,7 @@ The HTTP API is the main part for interacting with the application. Default addr
|
||||
- you can enable HTTP API only on localhost with `listen: "127.0.0.1:1984"` setting
|
||||
- you can change API `base_path` and host go2rtc on your main app webserver suburl
|
||||
- all files from `static_dir` hosted on root path: `/`
|
||||
- you can use raw TLS cert/key content or path to files
|
||||
|
||||
```yaml
|
||||
api:
|
||||
@@ -753,6 +795,17 @@ By default go2rtc provide RTSP-stream with only one first video and only one fir
|
||||
|
||||
Read more about [codecs filters](#codecs-filters).
|
||||
|
||||
### Module: RTMP
|
||||
|
||||
You can get any stream as RTMP-stream: `rtmp://192.168.1.123/{stream_name}`. Only H264/AAC codecs supported right now.
|
||||
|
||||
[Incoming stream](#incoming-sources) in RTMP-format tested only with [OBS Studio](https://obsproject.com/) and Dahua camera. Different FFmpeg versions has differnt problems with this format.
|
||||
|
||||
```yaml
|
||||
rtmp:
|
||||
listen: ":1935" # by default - disabled!
|
||||
```
|
||||
|
||||
### Module: WebRTC
|
||||
|
||||
In most cases [WebRTC](https://en.wikipedia.org/wiki/WebRTC) uses direct peer-to-peer connection from your browser to go2rtc and sends media data via UDP.
|
||||
|
@@ -1,4 +1,4 @@
|
||||
openapi: 3.0.0
|
||||
openapi: 3.1.0
|
||||
|
||||
info:
|
||||
title: go2rtc
|
||||
@@ -111,7 +111,9 @@ paths:
|
||||
required: false
|
||||
schema: { type: integer }
|
||||
example: 100
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
|
||||
|
||||
@@ -130,14 +132,18 @@ paths:
|
||||
requestBody:
|
||||
content:
|
||||
"*/*": { example: "streams:..." }
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
patch:
|
||||
summary: Merge changes to main config file
|
||||
tags: [ Config ]
|
||||
requestBody:
|
||||
content:
|
||||
"*/*": { example: "streams:..." }
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
|
||||
|
||||
@@ -166,7 +172,9 @@ paths:
|
||||
required: false
|
||||
schema: { type: string }
|
||||
example: camera1
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
patch:
|
||||
summary: Update stream source
|
||||
tags: [ Streams list ]
|
||||
@@ -183,7 +191,9 @@ paths:
|
||||
required: true
|
||||
schema: { type: string }
|
||||
example: camera1
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
delete:
|
||||
summary: Delete stream
|
||||
tags: [ Streams list ]
|
||||
@@ -194,7 +204,9 @@ paths:
|
||||
required: true
|
||||
schema: { type: string }
|
||||
example: camera1
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
post:
|
||||
summary: Send stream from source to destination
|
||||
description: "[Stream to camera](https://github.com/AlexxIT/go2rtc#stream-to-camera)"
|
||||
@@ -212,7 +224,9 @@ paths:
|
||||
required: true
|
||||
schema: { type: string }
|
||||
example: camera1
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
|
||||
|
||||
@@ -347,7 +361,9 @@ paths:
|
||||
tags: [ Produce stream ]
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stream_dst_path"
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
/api/stream.flv?dst={dst}:
|
||||
post:
|
||||
summary: Post stream in FLV format
|
||||
@@ -355,7 +371,9 @@ paths:
|
||||
tags: [ Produce stream ]
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stream_dst_path"
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
/api/stream.ts?dst={dst}:
|
||||
post:
|
||||
summary: Post stream in MPEG-TS format
|
||||
@@ -363,7 +381,9 @@ paths:
|
||||
tags: [ Produce stream ]
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stream_dst_path"
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
/api/stream.mjpeg?dst={dst}:
|
||||
post:
|
||||
summary: Post stream in MJPEG format
|
||||
@@ -371,7 +391,9 @@ paths:
|
||||
tags: [ Produce stream ]
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stream_dst_path"
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
|
||||
|
||||
@@ -380,49 +402,65 @@ paths:
|
||||
summary: DVRIP cameras discovery
|
||||
description: "[Source: DVRIP](https://github.com/AlexxIT/go2rtc#source-dvrip)"
|
||||
tags: [ Discovery ]
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
/api/ffmpeg/devices:
|
||||
get:
|
||||
summary: FFmpeg USB devices discovery
|
||||
description: "[Source: FFmpeg Device](https://github.com/AlexxIT/go2rtc#source-ffmpeg-device)"
|
||||
tags: [ Discovery ]
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
/api/ffmpeg/hardware:
|
||||
get:
|
||||
summary: FFmpeg hardware transcoding discovery
|
||||
description: "[Hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration)"
|
||||
tags: [ Discovery ]
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
/api/hass:
|
||||
get:
|
||||
summary: Home Assistant cameras discovery
|
||||
description: "[Source: Hass](https://github.com/AlexxIT/go2rtc#source-hass)"
|
||||
tags: [ Discovery ]
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
/api/homekit:
|
||||
get:
|
||||
summary: HomeKit cameras discovery
|
||||
description: "[Source: HomeKit](https://github.com/AlexxIT/go2rtc#source-homekit)"
|
||||
tags: [ Discovery ]
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
/api/nest:
|
||||
get:
|
||||
summary: Nest cameras discovery
|
||||
tags: [ Discovery ]
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
/api/onvif:
|
||||
get:
|
||||
summary: ONVIF cameras discovery
|
||||
description: "[Source: ONVIF](https://github.com/AlexxIT/go2rtc#source-onvif)"
|
||||
tags: [ Discovery ]
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
/api/roborock:
|
||||
get:
|
||||
summary: Roborock vacuums discovery
|
||||
description: "[Source: Roborock](https://github.com/AlexxIT/go2rtc#source-roborock)"
|
||||
tags: [ Discovery ]
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
|
||||
|
||||
@@ -431,7 +469,9 @@ paths:
|
||||
summary: ONVIF server implementation
|
||||
description: Simple realisation of the ONVIF protocol. Accepts any suburl requests
|
||||
tags: [ ONVIF ]
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
|
||||
|
||||
@@ -440,7 +480,9 @@ paths:
|
||||
summary: RTSPtoWebRTC server implementation
|
||||
description: Simple API for support [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration
|
||||
tags: [ RTSPtoWebRTC ]
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
|
||||
|
||||
@@ -465,7 +507,9 @@ paths:
|
||||
tags: [ WebTorrent ]
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stream_src_path"
|
||||
responses: { }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
/api/webtorrent:
|
||||
get:
|
||||
|
123
examples/homekit_info/main.go
Normal file
123
examples/homekit_info/main.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||
)
|
||||
|
||||
var servs = map[string]string{
|
||||
"3E": "Accessory Information",
|
||||
"7E": "Security System",
|
||||
"85": "Motion Sensor",
|
||||
"96": "Battery",
|
||||
"A2": "Protocol Information",
|
||||
"110": "Camera RTP Stream Management",
|
||||
"112": "Microphone",
|
||||
"113": "Speaker",
|
||||
"121": "Doorbell",
|
||||
"129": "Data Stream Transport Management",
|
||||
"204": "Camera Recording Management",
|
||||
"21A": "Camera Operating Mode",
|
||||
"22A": "Wi-Fi Transport",
|
||||
"239": "Accessory Runtime Information",
|
||||
}
|
||||
|
||||
var chars = map[string]string{
|
||||
"14": "Identify",
|
||||
"20": "Manufacturer",
|
||||
"21": "Model",
|
||||
"23": "Name",
|
||||
"30": "Serial Number",
|
||||
"52": "Firmware Revision",
|
||||
"53": "Hardware Revision",
|
||||
"220": "Product Data",
|
||||
"A6": "Accessory Flags",
|
||||
|
||||
"22": "Motion Detected",
|
||||
"75": "Status Active",
|
||||
|
||||
"11A": "Mute",
|
||||
"119": "Volume",
|
||||
|
||||
"B0": "Active",
|
||||
"209": "Selected Camera Recording Configuration",
|
||||
"207": "Supported Audio Recording Configuration",
|
||||
"205": "Supported Camera Recording Configuration",
|
||||
"206": "Supported Video Recording Configuration",
|
||||
"226": "Recording Audio Active",
|
||||
|
||||
"223": "Event Snapshots Active",
|
||||
"225": "Periodic Snapshots Active",
|
||||
"21B": "HomeKit Camera Active",
|
||||
"21C": "Third Party Camera Active",
|
||||
"21D": "Camera Operating Mode Indicator",
|
||||
"11B": "Night Vision",
|
||||
"129": "Supported Data Stream Transport Configuration",
|
||||
"37": "Version",
|
||||
"131": "Setup Data Stream Transport",
|
||||
"130": "Supported Data Stream Transport Configuration",
|
||||
|
||||
"120": "Streaming Status",
|
||||
"115": "Supported Audio Stream Configuration",
|
||||
"116": "Supported RTP Configuration",
|
||||
"114": "Supported Video Stream Configuration",
|
||||
"117": "Selected RTP Stream Configuration",
|
||||
"118": "Setup Endpoints",
|
||||
|
||||
"22B": "Current Transport",
|
||||
"22C": "Wi-Fi Capabilities",
|
||||
"22D": "Wi-Fi Configuration Control",
|
||||
|
||||
"23C": "Ping",
|
||||
|
||||
"68": "Battery Level",
|
||||
"79": "Status Low Battery",
|
||||
"8F": "Charging State",
|
||||
|
||||
"73": "Programmable Switch Event",
|
||||
"232": "Operating State Response",
|
||||
|
||||
"66": "Security System Current State",
|
||||
"67": "Security System Target State",
|
||||
}
|
||||
|
||||
func main() {
|
||||
src := os.Args[1]
|
||||
dst := os.Args[2]
|
||||
|
||||
f, err := os.Open(src)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var v hap.JSONAccessories
|
||||
if err = json.NewDecoder(f).Decode(&v); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, acc := range v.Value {
|
||||
for _, srv := range acc.Services {
|
||||
if srv.Desc == "" {
|
||||
srv.Desc = servs[srv.Type]
|
||||
}
|
||||
for _, chr := range srv.Characters {
|
||||
if chr.Desc == "" {
|
||||
chr.Desc = chars[chr.Type]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f, err = os.Create(dst)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
enc := json.NewEncoder(f)
|
||||
enc.SetIndent("", " ")
|
||||
if err = enc.Encode(v); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
@@ -12,6 +12,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
@@ -48,6 +49,7 @@ func Init() {
|
||||
HandleFunc("api", apiHandler)
|
||||
HandleFunc("api/config", configHandler)
|
||||
HandleFunc("api/exit", exitHandler)
|
||||
HandleFunc("api/restart", restartHandler)
|
||||
|
||||
// ensure we can listen without errors
|
||||
var err error
|
||||
@@ -83,7 +85,14 @@ func Init() {
|
||||
|
||||
// Initialize the HTTPS server
|
||||
if cfg.Mod.TLSListen != "" && cfg.Mod.TLSCert != "" && cfg.Mod.TLSKey != "" {
|
||||
cert, err := tls.X509KeyPair([]byte(cfg.Mod.TLSCert), []byte(cfg.Mod.TLSKey))
|
||||
var cert tls.Certificate
|
||||
if strings.IndexByte(cfg.Mod.TLSCert, '\n') < 0 && strings.IndexByte(cfg.Mod.TLSKey, '\n') < 0 {
|
||||
// check if file path
|
||||
cert, err = tls.LoadX509KeyPair(cfg.Mod.TLSCert, cfg.Mod.TLSKey)
|
||||
} else {
|
||||
// if text file content
|
||||
cert, err = tls.X509KeyPair([]byte(cfg.Mod.TLSCert), []byte(cfg.Mod.TLSKey))
|
||||
}
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
@@ -222,6 +231,15 @@ func exitHandler(w http.ResponseWriter, r *http.Request) {
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func restartHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
go shell.Restart()
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
|
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var Version = "1.7.1"
|
||||
var Version = ""
|
||||
var UserAgent = "go2rtc/" + Version
|
||||
|
||||
var ConfigPath string
|
||||
|
@@ -23,17 +23,11 @@ func Init() {
|
||||
}
|
||||
|
||||
func handle(url string) (core.Producer, error) {
|
||||
conn := dvrip.NewClient(url)
|
||||
if err := conn.Dial(); err != nil {
|
||||
client, err := dvrip.Dial(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := conn.Play(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := conn.Handle(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
return client, nil
|
||||
}
|
||||
|
||||
const Port = 34569 // UDP port number for dvrip discovery
|
||||
|
@@ -52,7 +52,8 @@ var defaults = map[string]string{
|
||||
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
|
||||
// `-tune zerolatency` - for minimal latency
|
||||
// `-profile high -level 4.1` - most used streaming profile
|
||||
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuvj420p",
|
||||
// `-pix_fmt:v yuv420p` - important for Telegram
|
||||
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p",
|
||||
"h265": "-c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency",
|
||||
"mjpeg": "-c:v mjpeg",
|
||||
//"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
|
||||
|
@@ -13,7 +13,7 @@ func TestParseArgsFile(t *testing.T) {
|
||||
|
||||
// [FILE] video will be transcoded to H264, audio will be skipped
|
||||
args = parseArgs("/media/bbb.mp4#video=h264")
|
||||
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuvj420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [FILE] video will be copied, audio will be transcoded to pcmu
|
||||
args = parseArgs("/media/bbb.mp4#video=copy#audio=pcmu")
|
||||
@@ -38,8 +38,9 @@ func TestParseArgsDevice(t *testing.T) {
|
||||
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i video="0" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [DEVICE] video will be transcoded to H265 with framerate 20, audio will be skipped
|
||||
args = parseArgs("device?video=0&video_size=1280x720&framerate=20#video=h265#audio=pcma")
|
||||
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1280x720 -framerate 20 -i video="0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -c:a pcm_alaw -ar:a 8000 -ac:a 1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
//args = parseArgs("device?video=0&video_size=1280x720&framerate=20#video=h265#audio=pcma")
|
||||
args = parseArgs("device?video=0&framerate=20#video=h265#audio=pcma")
|
||||
require.Equal(t, `ffmpeg -hide_banner -f dshow -framerate 20 -i video="0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -c:a pcm_alaw -ar:a 8000 -ac:a 1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
}
|
||||
|
||||
func TestParseArgsIpCam(t *testing.T) {
|
||||
@@ -49,7 +50,7 @@ func TestParseArgsIpCam(t *testing.T) {
|
||||
|
||||
// [HTTP-MJPEG] video will be transcoded to H264
|
||||
args = parseArgs("http://example.com#video=h264")
|
||||
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuvj420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [HLS] video will be copied, audio will be skipped
|
||||
args = parseArgs("https://example.com#video=copy")
|
||||
@@ -83,7 +84,7 @@ func TestParseArgsAudio(t *testing.T) {
|
||||
|
||||
// [AUDIO] audio will be transcoded to OPUS, video will be skipped
|
||||
args = parseArgs("rtsp:///example.com#audio=opus")
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a libopus -ar:a 48000 -ac:a 2 -application:a voip -min_comp 0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a libopus -application:a lowdelay -frame_duration 20 -min_comp 0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [AUDIO] audio will be transcoded to PCMU, video will be skipped
|
||||
args = parseArgs("rtsp:///example.com#audio=pcmu")
|
||||
@@ -113,23 +114,23 @@ func TestParseArgsAudio(t *testing.T) {
|
||||
func TestParseArgsHwVaapi(t *testing.T) {
|
||||
// [HTTP-MJPEG] video will be transcoded to H264
|
||||
args := parseArgs("http:///example.com#video=h264#hardware=vaapi")
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [RTSP] video with rotation, should be transcoded, so select H264
|
||||
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=vaapi")
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,transpose_vaapi=4" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,transpose_vaapi=4,scale_vaapi=out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
|
||||
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720#hardware=vaapi")
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=1280:720:out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [FILE] video will be output for MJPEG to pipe, audio will be skipped
|
||||
args = parseArgs("/media/bbb.mp4#video=mjpeg#hardware=vaapi")
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -re -i /media/bbb.mp4 -c:v mjpeg_vaapi -an -vf "format=vaapi|nv12,hwupload" -f mjpeg -`, args.String())
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -re -i /media/bbb.mp4 -c:v mjpeg_vaapi -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_range=tv" -f mjpeg -`, args.String())
|
||||
|
||||
// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265
|
||||
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=vaapi")
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
}
|
||||
|
||||
func TestParseArgsHwV4l2m2m(t *testing.T) {
|
||||
@@ -207,3 +208,8 @@ func TestParseArgsHwVideotoolbox(t *testing.T) {
|
||||
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=videotoolbox")
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
}
|
||||
|
||||
func TestDeckLink(t *testing.T) {
|
||||
args := parseArgs(`DeckLink SDI (2)#video=h264#hardware=vaapi#input=-format_code Hp29 -f decklink -i "{input}"`)
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -format_code Hp29 -f decklink -i "DeckLink SDI (2)" -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
}
|
||||
|
@@ -58,6 +58,8 @@ func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string)
|
||||
case EngineVAAPI:
|
||||
args.Codecs[i] = defaults[name+"/"+engine]
|
||||
|
||||
fixYCbCrRange(args)
|
||||
|
||||
if !args.HasFilters("drawtext=") {
|
||||
args.Input = "-hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch " + args.Input
|
||||
|
||||
@@ -154,3 +156,21 @@ func cut(s string, sep byte, pos int) string {
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// fixYCbCrRange convert jpeg/pc range to mpeg/tv range
|
||||
// vaapi(pc, bt709, progressive) == yuvj420p (jpeg/full/pc)
|
||||
// vaapi(tv, bt709, progressive) == yuv420p (mpeg/limited/tv)
|
||||
// https://ffmpeg.org/ffmpeg-all.html#scale-1
|
||||
func fixYCbCrRange(args *ffmpeg.Args) {
|
||||
for i, filter := range args.Filters {
|
||||
if strings.HasPrefix(filter, "scale=") {
|
||||
if !strings.Contains(filter, "out_range=") {
|
||||
args.Filters[i] = filter + ":out_range=tv"
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// scale=out_color_matrix=bt709:out_range=tv
|
||||
args.Filters = append(args.Filters, "scale=out_range=tv")
|
||||
}
|
||||
|
@@ -198,9 +198,11 @@ func (s *server) AddPair(conn net.Conn, id string, public []byte, permissions by
|
||||
"client_public": []string{hex.EncodeToString(public)},
|
||||
"permissions": []string{string('0' + permissions)},
|
||||
}
|
||||
s.pairings = append(s.pairings, query.Encode())
|
||||
s.UpdateStatus()
|
||||
s.PatchConfig()
|
||||
if s.GetPair(conn, id) == nil {
|
||||
s.pairings = append(s.pairings, query.Encode())
|
||||
s.UpdateStatus()
|
||||
s.PatchConfig()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) DelPair(conn net.Conn, id string) {
|
||||
|
@@ -1,39 +1,188 @@
|
||||
package rtmp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/flv"
|
||||
"github.com/AlexxIT/go2rtc/pkg/rtmp"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
var conf struct {
|
||||
Mod struct {
|
||||
Listen string `yaml:"listen" json:"listen"`
|
||||
} `yaml:"rtmp"`
|
||||
}
|
||||
|
||||
app.LoadConfig(&conf)
|
||||
|
||||
log = app.GetLogger("rtmp")
|
||||
|
||||
streams.HandleFunc("rtmp", streamsHandle)
|
||||
streams.HandleFunc("rtmps", streamsHandle)
|
||||
streams.HandleFunc("rtmpx", streamsHandle)
|
||||
|
||||
api.HandleFunc("api/stream.flv", apiHandle)
|
||||
|
||||
streams.HandleConsumerFunc("rtmp", streamsConsumerHandle)
|
||||
streams.HandleConsumerFunc("rtmps", streamsConsumerHandle)
|
||||
streams.HandleConsumerFunc("rtmpx", streamsConsumerHandle)
|
||||
|
||||
address := conf.Mod.Listen
|
||||
if address == "" {
|
||||
return
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Str("addr", address).Msg("[rtmp] listen")
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err = tcpHandle(conn); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
}
|
||||
}()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func tcpHandle(netConn net.Conn) error {
|
||||
rtmpConn, err := rtmp.NewServer(netConn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = rtmpConn.ReadCommands(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch rtmpConn.Intent {
|
||||
case rtmp.CommandPlay:
|
||||
stream := streams.Get(rtmpConn.App)
|
||||
if stream == nil {
|
||||
return errors.New("stream not found: " + rtmpConn.App)
|
||||
}
|
||||
|
||||
cons := flv.NewConsumer()
|
||||
if err = stream.AddConsumer(cons); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer stream.RemoveConsumer(cons)
|
||||
|
||||
if err = rtmpConn.WriteStart(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = cons.WriteTo(rtmpConn)
|
||||
|
||||
return nil
|
||||
|
||||
case rtmp.CommandPublish:
|
||||
stream := streams.Get(rtmpConn.App)
|
||||
if stream == nil {
|
||||
return errors.New("stream not found: " + rtmpConn.App)
|
||||
}
|
||||
|
||||
if err = rtmpConn.WriteStart(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prod, err := rtmpConn.Producer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stream.AddProducer(prod)
|
||||
|
||||
defer stream.RemoveProducer(prod)
|
||||
|
||||
_ = prod.Start()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("rtmp: unknown command: " + rtmpConn.Intent)
|
||||
}
|
||||
|
||||
var log zerolog.Logger
|
||||
|
||||
func streamsHandle(url string) (core.Producer, error) {
|
||||
client, err := rtmp.Dial(url)
|
||||
client, err := rtmp.DialPlay(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func streamsConsumerHandle(url string) (core.Consumer, func(), error) {
|
||||
cons := flv.NewConsumer()
|
||||
run := func() {
|
||||
wr, err := rtmp.DialPublish(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = cons.WriteTo(wr)
|
||||
}
|
||||
|
||||
return cons, run, nil
|
||||
}
|
||||
|
||||
func apiHandle(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "", http.StatusMethodNotAllowed)
|
||||
outputFLV(w, r)
|
||||
} else {
|
||||
inputFLV(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func outputFLV(w http.ResponseWriter, r *http.Request) {
|
||||
src := r.URL.Query().Get("src")
|
||||
stream := streams.Get(src)
|
||||
if stream == nil {
|
||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
cons := flv.NewConsumer()
|
||||
cons.Type = "HTTP-FLV consumer"
|
||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
||||
cons.UserAgent = r.UserAgent()
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
|
||||
h := w.Header()
|
||||
h.Set("Content-Type", "video/x-flv")
|
||||
|
||||
_, _ = cons.WriteTo(w)
|
||||
|
||||
stream.RemoveConsumer(cons)
|
||||
}
|
||||
|
||||
func inputFLV(w http.ResponseWriter, r *http.Request) {
|
||||
dst := r.URL.Query().Get("dst")
|
||||
stream := streams.Get(dst)
|
||||
if stream == nil {
|
||||
|
@@ -73,3 +73,25 @@ func Location(url string) (string, error) {
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// TODO: rework
|
||||
|
||||
type ConsumerHandler func(url string) (core.Consumer, func(), error)
|
||||
|
||||
var consumerHandlers = map[string]ConsumerHandler{}
|
||||
|
||||
func HandleConsumerFunc(scheme string, handler ConsumerHandler) {
|
||||
consumerHandlers[scheme] = handler
|
||||
}
|
||||
|
||||
func GetConsumer(url string) (core.Consumer, func(), error) {
|
||||
if i := strings.IndexByte(url, ':'); i > 0 {
|
||||
scheme := url[:i]
|
||||
|
||||
if handler, ok := consumerHandlers[scheme]; ok {
|
||||
return handler(url)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil, errors.New("streams: unsupported scheme: " + url)
|
||||
}
|
||||
|
38
internal/streams/publish.go
Normal file
38
internal/streams/publish.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package streams
|
||||
|
||||
import "time"
|
||||
|
||||
func (s *Stream) Publish(url string) error {
|
||||
cons, run, err := GetConsumer(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = s.AddConsumer(cons); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
run()
|
||||
s.RemoveConsumer(cons)
|
||||
|
||||
// TODO: more smart retry
|
||||
time.Sleep(5 * time.Second)
|
||||
_ = s.Publish(url)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Publish(stream *Stream, destination any) {
|
||||
switch v := destination.(type) {
|
||||
case string:
|
||||
if err := stream.Publish(v); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
}
|
||||
case []any:
|
||||
for _, v := range v {
|
||||
Publish(stream, v)
|
||||
}
|
||||
}
|
||||
}
|
@@ -5,6 +5,7 @@ import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
@@ -13,18 +14,31 @@ import (
|
||||
|
||||
func Init() {
|
||||
var cfg struct {
|
||||
Mod map[string]any `yaml:"streams"`
|
||||
Streams map[string]any `yaml:"streams"`
|
||||
Publish map[string]any `yaml:"publish"`
|
||||
}
|
||||
|
||||
app.LoadConfig(&cfg)
|
||||
|
||||
log = app.GetLogger("streams")
|
||||
|
||||
for name, item := range cfg.Mod {
|
||||
for name, item := range cfg.Streams {
|
||||
streams[name] = NewStream(item)
|
||||
}
|
||||
|
||||
api.HandleFunc("api/streams", streamsHandler)
|
||||
|
||||
if cfg.Publish == nil {
|
||||
return
|
||||
}
|
||||
|
||||
time.AfterFunc(time.Second, func() {
|
||||
for name, dst := range cfg.Publish {
|
||||
if stream := Get(name); stream != nil {
|
||||
Publish(stream, dst)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Get(name string) *Stream {
|
||||
@@ -172,6 +186,10 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
api.ResponseJSON(w, stream)
|
||||
}
|
||||
} else if stream = Get(src); stream != nil {
|
||||
if err := stream.Publish(dst); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
} else {
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
}
|
||||
|
2
main.go
2
main.go
@@ -34,6 +34,8 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
app.Version = "1.8.0"
|
||||
|
||||
// 1. Core modules: app, api/ws, streams
|
||||
|
||||
app.Init() // init config and logs
|
||||
|
@@ -21,6 +21,10 @@ func RTPDepay(handler core.HandlerFunc) core.HandlerFunc {
|
||||
|
||||
//log.Printf("[RTP/AAC] units: %d, size: %4d, ts: %10d, %t", headersSize/2, len(packet.Payload), packet.Timestamp, packet.Marker)
|
||||
|
||||
if len(packet.Payload) < int(2+headersSize) {
|
||||
return
|
||||
}
|
||||
|
||||
headers := packet.Payload[2 : 2+headersSize]
|
||||
units := packet.Payload[2+headersSize:]
|
||||
|
||||
|
@@ -3,6 +3,7 @@ package core
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@@ -32,6 +33,8 @@ func (w *WriteBuffer) Write(p []byte) (n int, err error) {
|
||||
} else if n, err = w.Writer.Write(p); err != nil {
|
||||
w.err = err
|
||||
w.done()
|
||||
} else if f, ok := w.Writer.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
w.mu.Unlock()
|
||||
return
|
||||
|
@@ -2,8 +2,8 @@ package dvrip
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -12,49 +12,29 @@ import (
|
||||
"net"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264/annexb"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/pion/rtp"
|
||||
const (
|
||||
Login = 1000
|
||||
OPMonitorClaim = 1413
|
||||
OPMonitorStart = 1410
|
||||
OPTalkClaim = 1434
|
||||
OPTalkStart = 1430
|
||||
OPTalkData = 1432
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
core.Listener
|
||||
|
||||
uri string
|
||||
conn net.Conn
|
||||
reader *bufio.Reader
|
||||
session uint32
|
||||
seq uint32
|
||||
stream string
|
||||
|
||||
medias []*core.Media
|
||||
receivers []*core.Receiver
|
||||
videoTrack *core.Receiver
|
||||
audioTrack *core.Receiver
|
||||
|
||||
videoTS uint32
|
||||
videoDT uint32
|
||||
audioTS uint32
|
||||
audioSeq uint16
|
||||
|
||||
recv uint32
|
||||
rd io.Reader
|
||||
buf []byte
|
||||
}
|
||||
|
||||
type Response map[string]any
|
||||
|
||||
const Login = uint16(1000)
|
||||
const OPMonitorClaim = uint16(1413)
|
||||
const OPMonitorStart = uint16(1410)
|
||||
|
||||
func NewClient(url string) *Client {
|
||||
return &Client{uri: url}
|
||||
}
|
||||
|
||||
func (c *Client) Dial() (err error) {
|
||||
u, err := url.Parse(c.uri)
|
||||
func (c *Client) Dial(rawURL string) (err error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -69,26 +49,27 @@ func (c *Client) Dial() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
c.reader = bufio.NewReader(c.conn)
|
||||
if query := u.Query(); query.Get("backchannel") != "1" {
|
||||
channel := query.Get("channel")
|
||||
if channel == "" {
|
||||
channel = "0"
|
||||
}
|
||||
|
||||
query := u.Query()
|
||||
channel := query.Get("channel")
|
||||
if channel == "" {
|
||||
channel = "0"
|
||||
subtype := query.Get("subtype")
|
||||
switch subtype {
|
||||
case "", "0":
|
||||
subtype = "Main"
|
||||
case "1":
|
||||
subtype = "Extra1"
|
||||
}
|
||||
|
||||
c.stream = fmt.Sprintf(
|
||||
`{"Channel":%s,"CombinMode":"NONE","StreamType":"%s","TransMode":"TCP"}`,
|
||||
channel, subtype,
|
||||
)
|
||||
}
|
||||
|
||||
subtype := query.Get("subtype")
|
||||
switch subtype {
|
||||
case "", "0":
|
||||
subtype = "Main"
|
||||
case "1":
|
||||
subtype = "Extra1"
|
||||
}
|
||||
|
||||
c.stream = fmt.Sprintf(
|
||||
`{"Channel":%s,"CombinMode":"NONE","StreamType":"%s","TransMode":"TCP"}`,
|
||||
channel, subtype,
|
||||
)
|
||||
c.rd = bufio.NewReader(c.conn)
|
||||
|
||||
if u.User != nil {
|
||||
pass, _ := u.User.Password()
|
||||
@@ -98,210 +79,84 @@ func (c *Client) Dial() (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Login(user, pass string) (err error) {
|
||||
data := fmt.Sprintf(
|
||||
`{"EncryptType":"MD5","LoginType":"DVRIP-Web","PassWord":"%s","UserName":"%s"}`,
|
||||
SofiaHash(pass), user,
|
||||
)
|
||||
|
||||
if err = c.Request(Login, data); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = c.ResponseJSON()
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) Play() (err error) {
|
||||
format := `{"Name":"OPMonitor","SessionID":"0x%08X","OPMonitor":{"Action":"%s","Parameter":%s}}`
|
||||
|
||||
data := fmt.Sprintf(format, c.session, "Claim", c.stream)
|
||||
if err = c.Request(OPMonitorClaim, data); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = c.ResponseJSON(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
data = fmt.Sprintf(format, c.session, "Start", c.stream)
|
||||
return c.Request(OPMonitorStart, data)
|
||||
}
|
||||
|
||||
func (c *Client) Handle() error {
|
||||
var buf []byte
|
||||
var size int
|
||||
|
||||
var probe byte
|
||||
if c.medias == nil {
|
||||
probe = 1
|
||||
}
|
||||
|
||||
for {
|
||||
b, err := c.Response()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// collect data from multiple packets
|
||||
if size > 0 {
|
||||
buf = append(buf, b...)
|
||||
if len(buf) < size {
|
||||
continue
|
||||
}
|
||||
if len(buf) > size {
|
||||
return errors.New("wrong size")
|
||||
}
|
||||
b = buf
|
||||
}
|
||||
|
||||
dataType := binary.BigEndian.Uint32(b)
|
||||
switch dataType {
|
||||
case 0x1FC, 0x1FE:
|
||||
size = int(binary.LittleEndian.Uint32(b[12:])) + 16
|
||||
case 0x1FD: // PFrame
|
||||
size = int(binary.LittleEndian.Uint32(b[4:])) + 8
|
||||
case 0x1FA, 0x1F9:
|
||||
size = int(binary.LittleEndian.Uint16(b[6:])) + 8
|
||||
default:
|
||||
return fmt.Errorf("unknown type: %X", dataType)
|
||||
}
|
||||
|
||||
if len(b) < size {
|
||||
buf = b
|
||||
continue // need to collect data from next packets
|
||||
}
|
||||
|
||||
//log.Printf("[DVR] type: %d, len: %d", dataType, len(b))
|
||||
|
||||
switch dataType {
|
||||
case 0x1FC, 0x1FE: // video IFrame
|
||||
payload := annexb.EncodeToAVCC(b[16:], false)
|
||||
|
||||
if c.videoTrack == nil {
|
||||
fps := b[5]
|
||||
//width := uint16(b[6]) * 8
|
||||
//height := uint16(b[7]) * 8
|
||||
//println(width, height)
|
||||
ts := b[8:]
|
||||
|
||||
// the exact value of the start TS does not matter
|
||||
c.videoTS = binary.LittleEndian.Uint32(ts)
|
||||
c.videoDT = 90000 / uint32(fps)
|
||||
|
||||
c.AddVideoTrack(b[4], payload)
|
||||
}
|
||||
|
||||
if c.videoTrack != nil {
|
||||
c.videoTS += c.videoDT
|
||||
|
||||
packet := &rtp.Packet{
|
||||
Header: rtp.Header{Timestamp: c.videoTS},
|
||||
Payload: payload,
|
||||
}
|
||||
|
||||
//log.Printf("[AVC] %v, len: %d, ts: %10d", h265.Types(payload), len(payload), packet.Timestamp)
|
||||
|
||||
c.videoTrack.WriteRTP(packet)
|
||||
}
|
||||
|
||||
case 0x1FD: // PFrame
|
||||
if c.videoTrack != nil {
|
||||
c.videoTS += c.videoDT
|
||||
|
||||
packet := &rtp.Packet{
|
||||
Header: rtp.Header{Timestamp: c.videoTS},
|
||||
Payload: annexb.EncodeToAVCC(b[8:], false),
|
||||
}
|
||||
|
||||
//log.Printf("[DVR] %v, len: %d, ts: %10d", h265.Types(packet.Payload), len(packet.Payload), packet.Timestamp)
|
||||
|
||||
c.videoTrack.WriteRTP(packet)
|
||||
}
|
||||
|
||||
case 0x1FA, 0x1F9: // audio
|
||||
if c.audioTrack == nil {
|
||||
// the exact value of the start TS does not matter
|
||||
c.audioTS = c.videoTS
|
||||
|
||||
c.AddAudioTrack(b[4], b[5])
|
||||
}
|
||||
|
||||
if c.audioTrack != nil {
|
||||
for b != nil {
|
||||
payload := b[8:size]
|
||||
if len(b) > size {
|
||||
b = b[size:]
|
||||
} else {
|
||||
b = nil
|
||||
}
|
||||
|
||||
c.audioTS += uint32(len(payload))
|
||||
c.audioSeq++
|
||||
|
||||
packet := &rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
Marker: true,
|
||||
SequenceNumber: c.audioSeq,
|
||||
Timestamp: c.audioTS,
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
|
||||
//log.Printf("[DVR] len: %d, ts: %10d", len(packet.Payload), packet.Timestamp)
|
||||
|
||||
c.audioTrack.WriteRTP(packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if probe != 0 {
|
||||
probe++
|
||||
if (c.videoTS > 0 && c.audioTS > 0) || probe == 20 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
size = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func (c *Client) Request(cmd uint16, data string) (err error) {
|
||||
func (c *Client) Login(user, pass string) (err error) {
|
||||
data := fmt.Sprintf(
|
||||
`{"EncryptType":"MD5","LoginType":"DVRIP-Web","PassWord":"%s","UserName":"%s"}`+"\x0A\x00",
|
||||
SofiaHash(pass), user,
|
||||
)
|
||||
|
||||
if _, err = c.WriteCmd(Login, []byte(data)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = c.ReadJSON()
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) Play() error {
|
||||
format := `{"Name":"OPMonitor","SessionID":"0x%08X","OPMonitor":{"Action":"%s","Parameter":%s}}` + "\x0A\x00"
|
||||
|
||||
data := fmt.Sprintf(format, c.session, "Claim", c.stream)
|
||||
if _, err := c.WriteCmd(OPMonitorClaim, []byte(data)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.ReadJSON(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data = fmt.Sprintf(format, c.session, "Start", c.stream)
|
||||
_, err := c.WriteCmd(OPMonitorStart, []byte(data))
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) Talk() error {
|
||||
format := `{"Name":"OPTalk","SessionID":"0x%08X","OPTalk":{"Action":"%s"}}` + "\x0A\x00"
|
||||
|
||||
data := fmt.Sprintf(format, c.session, "Claim")
|
||||
if _, err := c.WriteCmd(OPTalkClaim, []byte(data)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.ReadJSON(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data = fmt.Sprintf(format, c.session, "Start")
|
||||
_, err := c.WriteCmd(OPTalkStart, []byte(data))
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) WriteCmd(cmd uint16, payload []byte) (n int, err error) {
|
||||
b := make([]byte, 20, 128)
|
||||
b[0] = 255
|
||||
binary.LittleEndian.PutUint32(b[4:], c.session)
|
||||
binary.LittleEndian.PutUint32(b[8:], c.seq)
|
||||
binary.LittleEndian.PutUint16(b[14:], cmd)
|
||||
binary.LittleEndian.PutUint32(b[16:], uint32(len(data))+2)
|
||||
b = append(b, data...)
|
||||
b = append(b, 0x0A, 0x00)
|
||||
binary.LittleEndian.PutUint32(b[16:], uint32(len(payload)))
|
||||
b = append(b, payload...)
|
||||
|
||||
c.seq++
|
||||
|
||||
if err = c.conn.SetWriteDeadline(time.Now().Add(time.Second * 5)); err != nil {
|
||||
return
|
||||
return 0, err
|
||||
}
|
||||
|
||||
_, err = c.conn.Write(b)
|
||||
return
|
||||
return c.conn.Write(b)
|
||||
}
|
||||
|
||||
func (c *Client) Response() (b []byte, err error) {
|
||||
func (c *Client) ReadChunk() (b []byte, err error) {
|
||||
if err = c.conn.SetReadDeadline(time.Now().Add(time.Second * 5)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b = make([]byte, 20)
|
||||
if _, err = io.ReadFull(c.reader, b); err != nil {
|
||||
if _, err = io.ReadFull(c.rd, b); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.recv += 20
|
||||
|
||||
if b[0] != 255 {
|
||||
return nil, errors.New("read error")
|
||||
}
|
||||
@@ -310,17 +165,59 @@ func (c *Client) Response() (b []byte, err error) {
|
||||
size := binary.LittleEndian.Uint32(b[16:])
|
||||
|
||||
b = make([]byte, size)
|
||||
if _, err = io.ReadFull(c.reader, b); err != nil {
|
||||
if _, err = io.ReadFull(c.rd, b); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.recv += size
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) ResponseJSON() (res Response, err error) {
|
||||
b, err := c.Response()
|
||||
func (c *Client) ReadPacket() (pType byte, payload []byte, err error) {
|
||||
var b []byte
|
||||
|
||||
// many cameras may split packet to multiple chunks
|
||||
// some rare cameras may put multiple packets to single chunk
|
||||
for len(c.buf) < 16 {
|
||||
if b, err = c.ReadChunk(); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
c.buf = append(c.buf, b...)
|
||||
}
|
||||
|
||||
if !bytes.HasPrefix(c.buf, []byte{0, 0, 1}) {
|
||||
return 0, nil, fmt.Errorf("dvrip: wrong packet: %0.16x", c.buf)
|
||||
}
|
||||
|
||||
var size int
|
||||
|
||||
switch pType = c.buf[3]; pType {
|
||||
case 0xFC, 0xFE:
|
||||
size = int(binary.LittleEndian.Uint32(c.buf[12:])) + 16
|
||||
case 0xFD: // PFrame
|
||||
size = int(binary.LittleEndian.Uint32(c.buf[4:])) + 8
|
||||
case 0xFA, 0xF9:
|
||||
size = int(binary.LittleEndian.Uint16(c.buf[6:])) + 8
|
||||
default:
|
||||
return 0, nil, fmt.Errorf("dvrip: unknown packet type: %X", pType)
|
||||
}
|
||||
|
||||
for len(c.buf) < size {
|
||||
if b, err = c.ReadChunk(); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
c.buf = append(c.buf, b...)
|
||||
}
|
||||
|
||||
payload = c.buf[:size]
|
||||
c.buf = c.buf[size:]
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type Response map[string]any
|
||||
|
||||
func (c *Client) ReadJSON() (res Response, err error) {
|
||||
b, err := c.ReadChunk()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -336,94 +233,6 @@ func (c *Client) ResponseJSON() (res Response, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) AddVideoTrack(mediaCode byte, payload []byte) {
|
||||
var codec *core.Codec
|
||||
switch mediaCode {
|
||||
case 0x02, 0x12:
|
||||
codec = &core.Codec{
|
||||
Name: core.CodecH264,
|
||||
ClockRate: 90000,
|
||||
PayloadType: core.PayloadTypeRAW,
|
||||
FmtpLine: h264.GetFmtpLine(payload),
|
||||
}
|
||||
|
||||
case 0x03, 0x13, 0x43, 0x53:
|
||||
codec = &core.Codec{
|
||||
Name: core.CodecH265,
|
||||
ClockRate: 90000,
|
||||
PayloadType: core.PayloadTypeRAW,
|
||||
FmtpLine: "profile-id=1",
|
||||
}
|
||||
|
||||
for {
|
||||
size := 4 + int(binary.BigEndian.Uint32(payload))
|
||||
|
||||
switch h265.NALUType(payload) {
|
||||
case h265.NALUTypeVPS:
|
||||
codec.FmtpLine += ";sprop-vps=" + base64.StdEncoding.EncodeToString(payload[4:size])
|
||||
case h265.NALUTypeSPS:
|
||||
codec.FmtpLine += ";sprop-sps=" + base64.StdEncoding.EncodeToString(payload[4:size])
|
||||
case h265.NALUTypePPS:
|
||||
codec.FmtpLine += ";sprop-pps=" + base64.StdEncoding.EncodeToString(payload[4:size])
|
||||
}
|
||||
|
||||
if size < len(payload) {
|
||||
payload = payload[size:]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
println("[DVRIP] unsupported video codec:", mediaCode)
|
||||
return
|
||||
}
|
||||
|
||||
media := &core.Media{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{codec},
|
||||
}
|
||||
c.medias = append(c.medias, media)
|
||||
|
||||
c.videoTrack = core.NewReceiver(media, codec)
|
||||
c.receivers = append(c.receivers, c.videoTrack)
|
||||
}
|
||||
|
||||
var sampleRates = []uint32{4000, 8000, 11025, 16000, 20000, 22050, 32000, 44100, 48000}
|
||||
|
||||
func (c *Client) AddAudioTrack(mediaCode byte, sampleRate byte) {
|
||||
// https://github.com/vigoss30611/buildroot-ltc/blob/master/system/qm/ipc/ProtocolService/src/ZhiNuo/inc/zn_dh_base_type.h
|
||||
// PCM8 = 7, G729, IMA_ADPCM, G711U, G721, PCM8_VWIS, MS_ADPCM, G711A, PCM16
|
||||
var codec *core.Codec
|
||||
switch mediaCode {
|
||||
case 10: // G711U
|
||||
codec = &core.Codec{
|
||||
Name: core.CodecPCMU,
|
||||
}
|
||||
case 14: // G711A
|
||||
codec = &core.Codec{
|
||||
Name: core.CodecPCMA,
|
||||
}
|
||||
default:
|
||||
println("[DVRIP] unsupported audio codec:", mediaCode)
|
||||
return
|
||||
}
|
||||
|
||||
if sampleRate <= byte(len(sampleRates)) {
|
||||
codec.ClockRate = sampleRates[sampleRate-1]
|
||||
}
|
||||
|
||||
media := &core.Media{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{codec},
|
||||
}
|
||||
c.medias = append(c.medias, media)
|
||||
|
||||
c.audioTrack = core.NewReceiver(media, codec)
|
||||
c.receivers = append(c.receivers, c.audioTrack)
|
||||
}
|
||||
|
||||
func SofiaHash(password string) string {
|
||||
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
|
84
pkg/dvrip/consumer.go
Normal file
84
pkg/dvrip/consumer.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package dvrip
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
core.SuperConsumer
|
||||
client *Client
|
||||
}
|
||||
|
||||
func (c *Consumer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
return nil, core.ErrCantGetTrack
|
||||
}
|
||||
|
||||
func (c *Consumer) Start() error {
|
||||
if err := c.client.conn.SetReadDeadline(time.Time{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b := make([]byte, 4096)
|
||||
for {
|
||||
if _, err := c.client.rd.Read(b); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Consumer) Stop() error {
|
||||
_ = c.SuperConsumer.Close()
|
||||
return c.client.Close()
|
||||
}
|
||||
|
||||
func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
|
||||
if err := c.client.Talk(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
const PacketSize = 320
|
||||
|
||||
buf := make([]byte, 8+PacketSize)
|
||||
binary.BigEndian.PutUint32(buf, 0x1FA)
|
||||
|
||||
switch track.Codec.Name {
|
||||
case core.CodecPCMU:
|
||||
buf[4] = 10
|
||||
case core.CodecPCMA:
|
||||
buf[4] = 14
|
||||
}
|
||||
|
||||
//for i, rate := range sampleRates {
|
||||
// if rate == track.Codec.ClockRate {
|
||||
// buf[5] = byte(i) + 1
|
||||
// break
|
||||
// }
|
||||
//}
|
||||
buf[5] = 2 // ClockRate=8000
|
||||
|
||||
binary.LittleEndian.PutUint16(buf[6:], PacketSize)
|
||||
|
||||
var payload []byte
|
||||
|
||||
sender := core.NewSender(media, track.Codec)
|
||||
sender.Handler = func(packet *rtp.Packet) {
|
||||
payload = append(payload, packet.Payload...)
|
||||
|
||||
for len(payload) >= PacketSize {
|
||||
buf = append(buf[:8], payload[:PacketSize]...)
|
||||
if n, err := c.client.WriteCmd(OPTalkData, buf); err != nil {
|
||||
c.Send += n
|
||||
}
|
||||
|
||||
payload = payload[PacketSize:]
|
||||
}
|
||||
}
|
||||
|
||||
sender.HandleRTP(track)
|
||||
c.Senders = append(c.Senders, sender)
|
||||
return nil
|
||||
}
|
33
pkg/dvrip/dvrip.go
Normal file
33
pkg/dvrip/dvrip.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package dvrip
|
||||
|
||||
import "github.com/AlexxIT/go2rtc/pkg/core"
|
||||
|
||||
func Dial(url string) (core.Producer, error) {
|
||||
client := &Client{}
|
||||
if err := client.Dial(url); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if client.stream != "" {
|
||||
prod := &Producer{client: client}
|
||||
prod.Type = "DVRIP active producer"
|
||||
if err := prod.probe(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return prod, nil
|
||||
} else {
|
||||
cons := &Consumer{client: client}
|
||||
cons.Type = "DVRIP active consumer"
|
||||
cons.Medias = []*core.Media{
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecPCMA, ClockRate: 8000, PayloadType: 8},
|
||||
{Name: core.CodecPCMU, ClockRate: 8000, PayloadType: 0},
|
||||
},
|
||||
},
|
||||
}
|
||||
return cons, nil
|
||||
}
|
||||
}
|
@@ -1,41 +1,266 @@
|
||||
package dvrip
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264/annexb"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
func (c *Client) GetMedias() []*core.Media {
|
||||
return c.medias
|
||||
type Producer struct {
|
||||
core.SuperProducer
|
||||
|
||||
client *Client
|
||||
|
||||
video, audio *core.Receiver
|
||||
|
||||
videoTS uint32
|
||||
videoDT uint32
|
||||
audioTS uint32
|
||||
audioSeq uint16
|
||||
}
|
||||
|
||||
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
for _, track := range c.receivers {
|
||||
if track.Codec == codec {
|
||||
return track, nil
|
||||
func (c *Producer) Start() error {
|
||||
for {
|
||||
pType, b, err := c.client.ReadPacket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//log.Printf("[DVR] type: %d, len: %d", dataType, len(b))
|
||||
|
||||
switch pType {
|
||||
case 0xFC, 0xFE, 0xFD:
|
||||
if c.video == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var payload []byte
|
||||
if pType != 0xFD {
|
||||
payload = b[16:] // iframe
|
||||
} else {
|
||||
payload = b[8:] // pframe
|
||||
}
|
||||
|
||||
c.videoTS += c.videoDT
|
||||
|
||||
packet := &rtp.Packet{
|
||||
Header: rtp.Header{Timestamp: c.videoTS},
|
||||
Payload: annexb.EncodeToAVCC(payload, false),
|
||||
}
|
||||
|
||||
//log.Printf("[AVC] %v, len: %d, ts: %10d", h265.Types(payload), len(payload), packet.Timestamp)
|
||||
|
||||
c.video.WriteRTP(packet)
|
||||
|
||||
case 0xFA: // audio
|
||||
if c.audio == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
payload := b[8:]
|
||||
|
||||
c.audioTS += uint32(len(payload))
|
||||
c.audioSeq++
|
||||
|
||||
packet := &rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
Marker: true,
|
||||
SequenceNumber: c.audioSeq,
|
||||
Timestamp: c.audioTS,
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
|
||||
//log.Printf("[DVR] len: %d, ts: %10d", len(packet.Payload), packet.Timestamp)
|
||||
|
||||
c.audio.WriteRTP(packet)
|
||||
|
||||
case 0xF9: // unknown
|
||||
|
||||
default:
|
||||
println(fmt.Sprintf("dvrip: unknown packet type: %d", pType))
|
||||
}
|
||||
}
|
||||
return nil, core.ErrCantGetTrack
|
||||
}
|
||||
|
||||
func (c *Client) Start() error {
|
||||
return c.Handle()
|
||||
func (c *Producer) Stop() error {
|
||||
return c.client.Close()
|
||||
}
|
||||
|
||||
func (c *Client) Stop() error {
|
||||
for _, receiver := range c.receivers {
|
||||
receiver.Close()
|
||||
func (c *Producer) probe() error {
|
||||
if err := c.client.Play(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rd := core.NewReadBuffer(c.client.rd)
|
||||
rd.BufferSize = core.ProbeSize
|
||||
defer func() {
|
||||
c.client.buf = nil
|
||||
rd.Reset()
|
||||
}()
|
||||
|
||||
c.client.rd = rd
|
||||
|
||||
// some awful cameras has VERY rare keyframes
|
||||
// so we wait video+audio for default probe time
|
||||
// and wait anything for 15 seconds
|
||||
timeoutBoth := time.Now().Add(core.ProbeTimeout)
|
||||
timeoutAny := time.Now().Add(time.Second * 15)
|
||||
|
||||
for {
|
||||
if now := time.Now(); now.Before(timeoutBoth) {
|
||||
if c.video != nil && c.audio != nil {
|
||||
return nil
|
||||
}
|
||||
} else if now.Before(timeoutAny) {
|
||||
if c.video != nil || c.audio != nil {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
return errors.New("dvrip: can't probe medias")
|
||||
}
|
||||
|
||||
tag, b, err := c.client.ReadPacket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch tag {
|
||||
case 0xFC, 0xFE: // video
|
||||
if c.video != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
fps := b[5]
|
||||
//width := uint16(b[6]) * 8
|
||||
//height := uint16(b[7]) * 8
|
||||
//println(width, height)
|
||||
ts := b[8:]
|
||||
|
||||
// the exact value of the start TS does not matter
|
||||
c.videoTS = binary.LittleEndian.Uint32(ts)
|
||||
c.videoDT = 90000 / uint32(fps)
|
||||
|
||||
payload := annexb.EncodeToAVCC(b[16:], false)
|
||||
c.addVideoTrack(b[4], payload)
|
||||
|
||||
case 0xFA: // audio
|
||||
if c.audio != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// the exact value of the start TS does not matter
|
||||
c.audioTS = c.videoTS
|
||||
|
||||
c.addAudioTrack(b[4], b[5])
|
||||
}
|
||||
}
|
||||
return c.Close()
|
||||
}
|
||||
|
||||
func (c *Client) MarshalJSON() ([]byte, error) {
|
||||
info := &core.Info{
|
||||
Type: "DVRIP active producer",
|
||||
RemoteAddr: c.conn.RemoteAddr().String(),
|
||||
Medias: c.medias,
|
||||
Receivers: c.receivers,
|
||||
Recv: int(c.recv),
|
||||
func (c *Producer) addVideoTrack(mediaCode byte, payload []byte) {
|
||||
var codec *core.Codec
|
||||
switch mediaCode {
|
||||
case 0x02, 0x12:
|
||||
codec = &core.Codec{
|
||||
Name: core.CodecH264,
|
||||
ClockRate: 90000,
|
||||
PayloadType: core.PayloadTypeRAW,
|
||||
FmtpLine: h264.GetFmtpLine(payload),
|
||||
}
|
||||
|
||||
case 0x03, 0x13, 0x43, 0x53:
|
||||
codec = &core.Codec{
|
||||
Name: core.CodecH265,
|
||||
ClockRate: 90000,
|
||||
PayloadType: core.PayloadTypeRAW,
|
||||
FmtpLine: "profile-id=1",
|
||||
}
|
||||
|
||||
for {
|
||||
size := 4 + int(binary.BigEndian.Uint32(payload))
|
||||
|
||||
switch h265.NALUType(payload) {
|
||||
case h265.NALUTypeVPS:
|
||||
codec.FmtpLine += ";sprop-vps=" + base64.StdEncoding.EncodeToString(payload[4:size])
|
||||
case h265.NALUTypeSPS:
|
||||
codec.FmtpLine += ";sprop-sps=" + base64.StdEncoding.EncodeToString(payload[4:size])
|
||||
case h265.NALUTypePPS:
|
||||
codec.FmtpLine += ";sprop-pps=" + base64.StdEncoding.EncodeToString(payload[4:size])
|
||||
}
|
||||
|
||||
if size < len(payload) {
|
||||
payload = payload[size:]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
println("[DVRIP] unsupported video codec:", mediaCode)
|
||||
return
|
||||
}
|
||||
return json.Marshal(info)
|
||||
|
||||
media := &core.Media{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{codec},
|
||||
}
|
||||
c.Medias = append(c.Medias, media)
|
||||
|
||||
c.video = core.NewReceiver(media, codec)
|
||||
c.Receivers = append(c.Receivers, c.video)
|
||||
}
|
||||
|
||||
var sampleRates = []uint32{4000, 8000, 11025, 16000, 20000, 22050, 32000, 44100, 48000}
|
||||
|
||||
func (c *Producer) addAudioTrack(mediaCode byte, sampleRate byte) {
|
||||
// https://github.com/vigoss30611/buildroot-ltc/blob/master/system/qm/ipc/ProtocolService/src/ZhiNuo/inc/zn_dh_base_type.h
|
||||
// PCM8 = 7, G729, IMA_ADPCM, G711U, G721, PCM8_VWIS, MS_ADPCM, G711A, PCM16
|
||||
var codec *core.Codec
|
||||
switch mediaCode {
|
||||
case 10: // G711U
|
||||
codec = &core.Codec{
|
||||
Name: core.CodecPCMU,
|
||||
}
|
||||
case 14: // G711A
|
||||
codec = &core.Codec{
|
||||
Name: core.CodecPCMA,
|
||||
}
|
||||
default:
|
||||
println("[DVRIP] unsupported audio codec:", mediaCode)
|
||||
return
|
||||
}
|
||||
|
||||
if sampleRate <= byte(len(sampleRates)) {
|
||||
codec.ClockRate = sampleRates[sampleRate-1]
|
||||
}
|
||||
|
||||
media := &core.Media{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{codec},
|
||||
}
|
||||
c.Medias = append(c.Medias, media)
|
||||
|
||||
c.audio = core.NewReceiver(media, codec)
|
||||
c.Receivers = append(c.Receivers, c.audio)
|
||||
}
|
||||
|
||||
//func (c *Client) MarshalJSON() ([]byte, error) {
|
||||
// info := &core.Info{
|
||||
// Type: "DVRIP active producer",
|
||||
// RemoteAddr: c.conn.RemoteAddr().String(),
|
||||
// Medias: c.Medias,
|
||||
// Receivers: c.Receivers,
|
||||
// Recv: c.Recv,
|
||||
// }
|
||||
// return json.Marshal(info)
|
||||
//}
|
||||
|
@@ -60,6 +60,9 @@ func (a *AMF) ReadItem() (any, error) {
|
||||
case TypeObject:
|
||||
return a.ReadObject()
|
||||
|
||||
case TypeEcmaArray:
|
||||
return a.ReadEcmaArray()
|
||||
|
||||
case TypeNull:
|
||||
return nil, nil
|
||||
|
||||
@@ -174,7 +177,18 @@ func (a *AMF) WriteString(s string) {
|
||||
|
||||
func (a *AMF) WriteObject(obj map[string]any) {
|
||||
a.buf = append(a.buf, TypeObject)
|
||||
a.writeKV(obj)
|
||||
a.buf = append(a.buf, 0, 0, TypeObjectEnd)
|
||||
}
|
||||
|
||||
func (a *AMF) WriteEcmaArray(obj map[string]any) {
|
||||
n := len(obj)
|
||||
a.buf = append(a.buf, TypeEcmaArray, byte(n>>24), byte(n>>16), byte(n>>8), byte(n))
|
||||
a.writeKV(obj)
|
||||
a.buf = append(a.buf, 0, 0, TypeObjectEnd)
|
||||
}
|
||||
|
||||
func (a *AMF) writeKV(obj map[string]any) {
|
||||
for k, v := range obj {
|
||||
n := len(k)
|
||||
a.buf = append(a.buf, byte(n>>8), byte(n))
|
||||
@@ -185,16 +199,41 @@ func (a *AMF) WriteObject(obj map[string]any) {
|
||||
a.WriteString(v)
|
||||
case int:
|
||||
a.WriteNumber(float64(v))
|
||||
case uint16:
|
||||
a.WriteNumber(float64(v))
|
||||
case uint32:
|
||||
a.WriteNumber(float64(v))
|
||||
case float64:
|
||||
a.WriteNumber(v)
|
||||
case bool:
|
||||
a.WriteBool(v)
|
||||
default:
|
||||
panic(v)
|
||||
}
|
||||
}
|
||||
|
||||
a.buf = append(a.buf, 0, 0, TypeObjectEnd)
|
||||
}
|
||||
|
||||
func (a *AMF) WriteNull() {
|
||||
a.buf = append(a.buf, TypeNull)
|
||||
}
|
||||
|
||||
func EncodeItems(items ...any) []byte {
|
||||
a := &AMF{}
|
||||
for _, item := range items {
|
||||
switch v := item.(type) {
|
||||
case float64:
|
||||
a.WriteNumber(v)
|
||||
case int:
|
||||
a.WriteNumber(float64(v))
|
||||
case string:
|
||||
a.WriteString(v)
|
||||
case map[string]any:
|
||||
a.WriteObject(v)
|
||||
case nil:
|
||||
a.WriteNull()
|
||||
default:
|
||||
panic(v)
|
||||
}
|
||||
}
|
||||
return a.Bytes()
|
||||
}
|
||||
|
217
pkg/flv/amf/amf_test.go
Normal file
217
pkg/flv/amf/amf_test.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package amf
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewReader(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
actual string
|
||||
expect []any
|
||||
}{
|
||||
{
|
||||
name: "ffmpeg-http",
|
||||
actual: "02000a6f6e4d65746144617461080000001000086475726174696f6e000000000000000000000577696474680040940000000000000006686569676874004086800000000000000d766964656f646174617261746500409e62770000000000096672616d6572617465004038000000000000000c766964656f636f646563696400401c000000000000000d617564696f646174617261746500405ea93000000000000f617564696f73616d706c65726174650040e5888000000000000f617564696f73616d706c6573697a65004030000000000000000673746572656f0101000c617564696f636f6465636964004024000000000000000b6d616a6f725f6272616e640200046d703432000d6d696e6f725f76657273696f6e020001300011636f6d70617469626c655f6272616e647302000c69736f6d617663316d7034320007656e636f64657202000c4c61766636302e352e313030000866696c6573697a65000000000000000000000009",
|
||||
expect: []any{
|
||||
"onMetaData",
|
||||
map[string]any{
|
||||
"compatible_brands": "isomavc1mp42",
|
||||
"major_brand": "mp42",
|
||||
"minor_version": "0",
|
||||
"encoder": "Lavf60.5.100",
|
||||
|
||||
"filesize": float64(0),
|
||||
"duration": float64(0),
|
||||
|
||||
"videocodecid": float64(7),
|
||||
"width": float64(1280),
|
||||
"height": float64(720),
|
||||
"framerate": float64(24),
|
||||
"videodatarate": 1944.6162109375,
|
||||
|
||||
"audiocodecid": float64(10),
|
||||
"audiosamplerate": float64(44100),
|
||||
"stereo": true,
|
||||
"audiosamplesize": float64(16),
|
||||
"audiodatarate": 122.6435546875,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ffmpeg-file",
|
||||
actual: "02000a6f6e4d65746144617461080000000800086475726174696f6e004000000000000000000577696474680040940000000000000006686569676874004086800000000000000d766964656f646174617261746500000000000000000000096672616d6572617465004039000000000000000c766964656f636f646563696400401c0000000000000007656e636f64657202000c4c61766636302e352e313030000866696c6573697a6500411f541400000000000009",
|
||||
expect: []any{
|
||||
"onMetaData",
|
||||
map[string]any{
|
||||
"encoder": "Lavf60.5.100",
|
||||
|
||||
"filesize": float64(513285),
|
||||
"duration": float64(2),
|
||||
|
||||
"videocodecid": float64(7),
|
||||
"width": float64(1280),
|
||||
"height": float64(720),
|
||||
"framerate": float64(25),
|
||||
"videodatarate": float64(0),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reolink-1",
|
||||
actual: "0200075f726573756c74003ff0000000000000030006666d7356657202000d464d532f332c302c312c313233000c6361706162696c697469657300403f0000000000000000090300056c6576656c0200067374617475730004636f646502001d4e6574436f6e6e656374696f6e2e436f6e6e6563742e53756363657373000b6465736372697074696f6e020015436f6e6e656374696f6e207375636365656465642e000e6f626a656374456e636f64696e67000000000000000000000009",
|
||||
expect: []any{
|
||||
"_result", float64(1),
|
||||
map[string]any{
|
||||
"capabilities": float64(31),
|
||||
"fmsVer": "FMS/3,0,1,123",
|
||||
},
|
||||
map[string]any{
|
||||
"code": "NetConnection.Connect.Success",
|
||||
"description": "Connection succeeded.",
|
||||
"level": "status",
|
||||
"objectEncoding": float64(0),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reolink-2",
|
||||
actual: "0200075f726573756c7400400000000000000005003ff0000000000000",
|
||||
expect: []any{
|
||||
"_result", float64(2), nil, float64(1),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reolink-3",
|
||||
actual: "0200086f6e537461747573000000000000000000050300056c6576656c0200067374617475730004636f64650200144e657453747265616d2e506c61792e5374617274000b6465736372697074696f6e020015537461727420766964656f206f6e2064656d616e64000009",
|
||||
expect: []any{
|
||||
"onStatus", float64(0), nil,
|
||||
map[string]any{
|
||||
"code": "NetStream.Play.Start",
|
||||
"description": "Start video on demand",
|
||||
"level": "status",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reolink-4",
|
||||
actual: "0200117c52746d7053616d706c6541636365737301010101",
|
||||
expect: []any{
|
||||
"|RtmpSampleAccess", true, true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reolink-5",
|
||||
actual: "02000a6f6e4d6574614461746103000577696474680040a4000000000000000668656967687400409e000000000000000c646973706c617957696474680040a4000000000000000d646973706c617948656967687400409e00000000000000086475726174696f6e000000000000000000000c766964656f636f646563696400401c000000000000000c617564696f636f6465636964004024000000000000000f617564696f73616d706c65726174650040cf40000000000000096672616d657261746500403e000000000000000009",
|
||||
expect: []any{
|
||||
"onMetaData",
|
||||
map[string]any{
|
||||
"duration": float64(0),
|
||||
|
||||
"videocodecid": float64(7),
|
||||
"width": float64(2560),
|
||||
"height": float64(1920),
|
||||
"displayWidth": float64(2560),
|
||||
"displayHeight": float64(1920),
|
||||
"framerate": float64(30),
|
||||
|
||||
"audiocodecid": float64(10),
|
||||
"audiosamplerate": float64(16000),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mediamtx",
|
||||
actual: "02000d40736574446174614672616d6502000a6f6e4d6574614461746103000d766964656f6461746172617465000000000000000000000c766964656f636f646563696400401c000000000000000d617564696f6461746172617465000000000000000000000c617564696f636f6465636964004024000000000000000009",
|
||||
expect: []any{
|
||||
"@setDataFrame",
|
||||
"onMetaData",
|
||||
map[string]any{
|
||||
"videocodecid": float64(7),
|
||||
"videodatarate": float64(0),
|
||||
"audiocodecid": float64(10),
|
||||
"audiodatarate": float64(0),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "obs-connect",
|
||||
actual: "020007636f6e6e656374003ff000000000000003000361707002000c617070312f73747265616d3100047479706502000a6e6f6e70726976617465000e737570706f727473476f4177617901010008666c61736856657202001f464d4c452f332e302028636f6d70617469626c653b20464d53632f312e3029000673776655726c02002272746d703a2f2f3139322e3136382e31302e3130312f617070312f73747265616d310005746355726c02002272746d703a2f2f3139322e3136382e31302e3130312f617070312f73747265616d31000009",
|
||||
expect: []any{
|
||||
"connect", 1,
|
||||
map[string]any{
|
||||
"app": "app1/stream1",
|
||||
"flashVer": "FMLE/3.0 (compatible; FMSc/1.0)",
|
||||
"supportsGoAway": true,
|
||||
"swfUrl": "rtmp://192.168.10.101/app1/stream1",
|
||||
"tcUrl": "rtmp://192.168.10.101/app1/stream1",
|
||||
"type": "nonprivate",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "obs-key",
|
||||
actual: "02000d72656c6561736553747265616d004000000000000000050200046b657931",
|
||||
expect: []any{
|
||||
"releaseStream", float64(2), nil, "key1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "obs",
|
||||
actual: "02000d40736574446174614672616d6502000a6f6e4d65746144617461080000001400086475726174696f6e000000000000000000000866696c6553697a65000000000000000000000577696474680040840000000000000006686569676874004076800000000000000c766964656f636f646563696400401c000000000000000d766964656f64617461726174650040a388000000000000096672616d6572617465004039000000000000000c617564696f636f6465636964004024000000000000000d617564696f6461746172617465004064000000000000000f617564696f73616d706c65726174650040e5888000000000000f617564696f73616d706c6573697a65004030000000000000000d617564696f6368616e6e656c73004000000000000000000673746572656f01010003322e3101000003332e3101000003342e3001000003342e3101000003352e3101000003372e3101000007656e636f6465720200376f62732d6f7574707574206d6f64756c6520286c69626f62732076657273696f6e2032392e302e302d36322d6739303031323131663829000009",
|
||||
expect: []any{
|
||||
"@setDataFrame", "onMetaData", map[string]any{
|
||||
"2.1": false,
|
||||
"3.1": false,
|
||||
"4.0": false,
|
||||
"4.1": false,
|
||||
"5.1": false,
|
||||
"7.1": false,
|
||||
"audiochannels": float64(2),
|
||||
"audiocodecid": float64(10),
|
||||
"audiodatarate": float64(160),
|
||||
"audiosamplerate": float64(44100),
|
||||
"audiosamplesize": float64(16),
|
||||
"duration": float64(0),
|
||||
"encoder": "obs-output module (libobs version 29.0.0-62-g9001211f8)",
|
||||
"fileSize": float64(0),
|
||||
"framerate": float64(25),
|
||||
"height": float64(360),
|
||||
"stereo": true,
|
||||
"videocodecid": float64(7),
|
||||
"videodatarate": float64(2500),
|
||||
"width": float64(640),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "telegram-2",
|
||||
actual: "0200075f726573756c7400400000000000000005",
|
||||
expect: []any{
|
||||
"_result", float64(2), nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "telegram-4",
|
||||
actual: "0200075f726573756c7400401000000000000005003ff0000000000000",
|
||||
expect: []any{
|
||||
"_result", float64(4), nil, float64(1),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
b, err := hex.DecodeString(test.actual)
|
||||
require.Nil(t, err)
|
||||
|
||||
rd := NewReader(b)
|
||||
v, err := rd.ReadItems()
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, test.expect, v)
|
||||
})
|
||||
}
|
||||
}
|
93
pkg/flv/consumer.go
Normal file
93
pkg/flv/consumer.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package flv
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
core.SuperConsumer
|
||||
wr *core.WriteBuffer
|
||||
muxer *Muxer
|
||||
}
|
||||
|
||||
func NewConsumer() *Consumer {
|
||||
c := &Consumer{
|
||||
wr: core.NewWriteBuffer(nil),
|
||||
muxer: &Muxer{},
|
||||
}
|
||||
c.Medias = []*core.Media{
|
||||
{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecH264},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecAAC},
|
||||
},
|
||||
},
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||
sender := core.NewSender(media, track.Codec)
|
||||
|
||||
switch track.Codec.Name {
|
||||
case core.CodecH264:
|
||||
payload := c.muxer.GetPayloader(track.Codec)
|
||||
|
||||
sender.Handler = func(pkt *rtp.Packet) {
|
||||
b := payload(pkt)
|
||||
if n, err := c.wr.Write(b); err == nil {
|
||||
c.Send += n
|
||||
}
|
||||
}
|
||||
|
||||
if track.Codec.IsRTP() {
|
||||
sender.Handler = h264.RTPDepay(track.Codec, sender.Handler)
|
||||
} else {
|
||||
sender.Handler = h264.RepairAVCC(track.Codec, sender.Handler)
|
||||
}
|
||||
|
||||
case core.CodecAAC:
|
||||
payload := c.muxer.GetPayloader(track.Codec)
|
||||
|
||||
sender.Handler = func(pkt *rtp.Packet) {
|
||||
b := payload(pkt)
|
||||
if n, err := c.wr.Write(b); err == nil {
|
||||
c.Send += n
|
||||
}
|
||||
}
|
||||
|
||||
if track.Codec.IsRTP() {
|
||||
sender.Handler = aac.RTPDepay(sender.Handler)
|
||||
}
|
||||
}
|
||||
|
||||
sender.HandleRTP(track)
|
||||
c.Senders = append(c.Senders, sender)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Consumer) WriteTo(wr io.Writer) (int64, error) {
|
||||
b := c.muxer.GetInit()
|
||||
if _, err := wr.Write(b); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return c.wr.WriteTo(wr)
|
||||
}
|
||||
|
||||
func (c *Consumer) Stop() error {
|
||||
_ = c.SuperConsumer.Close()
|
||||
return c.wr.Close()
|
||||
}
|
172
pkg/flv/muxer.go
Normal file
172
pkg/flv/muxer.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package flv
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/flv/amf"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Muxer struct {
|
||||
codecs []*core.Codec
|
||||
}
|
||||
|
||||
const (
|
||||
FlagsVideo = 0b001
|
||||
FlagsAudio = 0b100
|
||||
)
|
||||
|
||||
func (m *Muxer) GetInit() []byte {
|
||||
b := []byte{
|
||||
'F', 'L', 'V', // signature
|
||||
1, // version
|
||||
0, // flags (has video/audio)
|
||||
0, 0, 0, 9, // header size
|
||||
0, 0, 0, 0, // tag 0 size
|
||||
}
|
||||
|
||||
obj := map[string]any{}
|
||||
|
||||
for _, codec := range m.codecs {
|
||||
switch codec.Name {
|
||||
case core.CodecH264:
|
||||
b[4] |= FlagsVideo
|
||||
obj["videocodecid"] = CodecAVC
|
||||
|
||||
case core.CodecAAC:
|
||||
b[4] |= FlagsAudio
|
||||
obj["audiocodecid"] = CodecAAC
|
||||
obj["audiosamplerate"] = codec.ClockRate
|
||||
obj["audiosamplesize"] = 16
|
||||
obj["stereo"] = codec.Channels == 2
|
||||
}
|
||||
}
|
||||
|
||||
data := amf.EncodeItems("@setDataFrame", "onMetaData", obj)
|
||||
b = append(b, EncodeTag(TagData, 0, data)...)
|
||||
|
||||
for _, codec := range m.codecs {
|
||||
switch codec.Name {
|
||||
case core.CodecH264:
|
||||
sps, pps := h264.GetParameterSet(codec.FmtpLine)
|
||||
if len(sps) == 0 {
|
||||
sps = []byte{0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2}
|
||||
}
|
||||
if len(pps) == 0 {
|
||||
pps = []byte{0x68, 0xce, 0x38, 0x80}
|
||||
}
|
||||
|
||||
config := h264.EncodeConfig(sps, pps)
|
||||
video := append(encodeAVData(codec, 0), config...)
|
||||
b = append(b, EncodeTag(TagVideo, 0, video)...)
|
||||
|
||||
case core.CodecAAC:
|
||||
s := core.Between(codec.FmtpLine, "config=", ";")
|
||||
config, _ := hex.DecodeString(s)
|
||||
audio := append(encodeAVData(codec, 0), config...)
|
||||
b = append(b, EncodeTag(TagAudio, 0, audio)...)
|
||||
}
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func (m *Muxer) GetPayloader(codec *core.Codec) func(packet *rtp.Packet) []byte {
|
||||
m.codecs = append(m.codecs, codec)
|
||||
|
||||
var ts0 uint32
|
||||
var k = codec.ClockRate / 1000
|
||||
|
||||
switch codec.Name {
|
||||
case core.CodecH264:
|
||||
buf := encodeAVData(codec, 1)
|
||||
|
||||
return func(packet *rtp.Packet) []byte {
|
||||
if h264.IsKeyframe(packet.Payload) {
|
||||
buf[0] = 1<<4 | 7
|
||||
} else {
|
||||
buf[0] = 2<<4 | 7
|
||||
}
|
||||
|
||||
buf = append(buf[:5], packet.Payload...) // reset buffer to previous place
|
||||
|
||||
if ts0 == 0 {
|
||||
ts0 = packet.Timestamp
|
||||
}
|
||||
|
||||
timeMS := (packet.Timestamp - ts0) / k
|
||||
return EncodeTag(TagVideo, timeMS, buf)
|
||||
}
|
||||
|
||||
case core.CodecAAC:
|
||||
buf := encodeAVData(codec, 1)
|
||||
|
||||
return func(packet *rtp.Packet) []byte {
|
||||
buf = append(buf[:2], packet.Payload...)
|
||||
|
||||
if ts0 == 0 {
|
||||
ts0 = packet.Timestamp
|
||||
}
|
||||
|
||||
timeMS := (packet.Timestamp - ts0) / k
|
||||
return EncodeTag(TagAudio, timeMS, buf)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func EncodeTag(tagType byte, timeMS uint32, payload []byte) []byte {
|
||||
payloadSize := uint32(len(payload))
|
||||
tagSize := payloadSize + 11
|
||||
|
||||
b := make([]byte, tagSize+4)
|
||||
b[0] = tagType
|
||||
b[1] = byte(payloadSize >> 16)
|
||||
b[2] = byte(payloadSize >> 8)
|
||||
b[3] = byte(payloadSize)
|
||||
b[4] = byte(timeMS >> 16)
|
||||
b[5] = byte(timeMS >> 8)
|
||||
b[6] = byte(timeMS)
|
||||
b[7] = byte(timeMS >> 24)
|
||||
copy(b[11:], payload)
|
||||
|
||||
binary.BigEndian.PutUint32(b[tagSize:], tagSize)
|
||||
return b
|
||||
}
|
||||
|
||||
func encodeAVData(codec *core.Codec, isFrame byte) []byte {
|
||||
switch codec.Name {
|
||||
case core.CodecH264:
|
||||
return []byte{
|
||||
1<<4 | 7, // keyframe + AVC
|
||||
isFrame, // 0 - config, 1 - frame
|
||||
0, 0, 0, // composition time = 0
|
||||
}
|
||||
|
||||
case core.CodecAAC:
|
||||
var b0 byte = 10 << 4 // AAC
|
||||
|
||||
switch codec.ClockRate {
|
||||
case 11025:
|
||||
b0 |= 1 << 2
|
||||
case 22050:
|
||||
b0 |= 2 << 2
|
||||
case 44100:
|
||||
b0 |= 3 << 2
|
||||
}
|
||||
|
||||
b0 |= 1 << 1 // 16 bits
|
||||
|
||||
if codec.Channels == 2 {
|
||||
b0 |= 1
|
||||
}
|
||||
|
||||
return []byte{b0, isFrame} // 0 - config, 1 - frame
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@@ -170,7 +170,10 @@ func (c *Producer) probe() error {
|
||||
if !bytes.Contains(pkt.Payload, []byte("onMetaData")) {
|
||||
waitType = append(waitType, TagData)
|
||||
}
|
||||
if bytes.Contains(pkt.Payload, []byte("videocodecid")) {
|
||||
// Dahua cameras doesn't send videocodecid
|
||||
if bytes.Contains(pkt.Payload, []byte("videocodecid")) ||
|
||||
bytes.Contains(pkt.Payload, []byte("width")) ||
|
||||
bytes.Contains(pkt.Payload, []byte("framerate")) {
|
||||
waitType = append(waitType, TagVideo)
|
||||
}
|
||||
if bytes.Contains(pkt.Payload, []byte("audiocodecid")) {
|
||||
|
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -83,3 +84,12 @@ func TestGetProfileLevelID(t *testing.T) {
|
||||
profile = GetProfileLevelID(s)
|
||||
require.Equal(t, "640029", profile)
|
||||
}
|
||||
|
||||
func TestDecodeSPS2(t *testing.T) {
|
||||
s := "6764001fad84010c20086100430802184010c200843b50740932"
|
||||
b, err := hex.DecodeString(s)
|
||||
require.Nil(t, err)
|
||||
|
||||
sps := DecodeSPS(b)
|
||||
assert.Nil(t, sps) // broken SPS?
|
||||
}
|
||||
|
@@ -115,9 +115,14 @@ func DecodeSPS(sps []byte) *SPS {
|
||||
s.seq_scaling_matrix_present_flag = r.ReadBit()
|
||||
if s.seq_scaling_matrix_present_flag != 0 {
|
||||
for i := byte(0); i < n; i++ {
|
||||
ssl := r.ReadBit() // seq_scaling_list_present_flag[i]
|
||||
if ssl != 0 {
|
||||
return nil // not implemented
|
||||
//goland:noinspection GoSnakeCaseUsage
|
||||
seq_scaling_list_present_flag := r.ReadBit()
|
||||
if seq_scaling_list_present_flag != 0 {
|
||||
if i < 6 {
|
||||
s.scaling_list(r, 16)
|
||||
} else {
|
||||
s.scaling_list(r, 64)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,3 +214,18 @@ func DecodeSPS(sps []byte) *SPS {
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
//goland:noinspection GoSnakeCaseUsage
|
||||
func (s *SPS) scaling_list(r *bits.Reader, sizeOfScalingList int) {
|
||||
lastScale := int32(8)
|
||||
nextScale := int32(8)
|
||||
for j := 0; j < sizeOfScalingList; j++ {
|
||||
if nextScale != 0 {
|
||||
delta_scale := r.ReadSEGolomb()
|
||||
nextScale = (lastScale + delta_scale + 256) % 256
|
||||
}
|
||||
if nextScale != 0 {
|
||||
lastScale = nextScale
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -93,6 +93,8 @@ func (a *Accessory) GetCharacterByID(iid uint64) *Character {
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
Desc string `json:"description,omitempty"`
|
||||
|
||||
Type string `json:"type"`
|
||||
IID uint64 `json:"iid"`
|
||||
Primary bool `json:"primary,omitempty"`
|
||||
|
@@ -13,13 +13,14 @@ import (
|
||||
// Value should be omit for PW
|
||||
// Value may be empty for PR
|
||||
type Character struct {
|
||||
Desc string `json:"description,omitempty"`
|
||||
|
||||
IID uint64 `json:"iid"`
|
||||
Type string `json:"type"`
|
||||
Format string `json:"format"`
|
||||
Value any `json:"value,omitempty"`
|
||||
Perms []string `json:"perms"`
|
||||
|
||||
//Descr string `json:"description,omitempty"`
|
||||
//MaxLen int `json:"maxLen,omitempty"`
|
||||
//Unit string `json:"unit,omitempty"`
|
||||
//MinValue any `json:"minValue,omitempty"`
|
||||
|
@@ -41,6 +41,9 @@ type Client struct {
|
||||
|
||||
Conn net.Conn
|
||||
reader *bufio.Reader
|
||||
|
||||
res chan *http.Response
|
||||
err error
|
||||
}
|
||||
|
||||
func NewClient(rawURL string) (*Client, error) {
|
||||
@@ -214,7 +217,7 @@ func (c *Client) Dial() (err error) {
|
||||
return
|
||||
}
|
||||
// new reader for new conn
|
||||
c.reader = bufio.NewReaderSize(c.Conn, 32*1024) // 32K like default request body
|
||||
c.reader = bufio.NewReader(c.Conn)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -223,9 +226,33 @@ func (c *Client) Close() error {
|
||||
if c.Conn == nil {
|
||||
return nil
|
||||
}
|
||||
conn := c.Conn
|
||||
c.Conn = nil
|
||||
return conn.Close()
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
func (c *Client) eventsReader() {
|
||||
c.res = make(chan *http.Response)
|
||||
|
||||
for {
|
||||
var res *http.Response
|
||||
if res, c.err = ReadResponse(c.reader, nil); c.err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
var body []byte
|
||||
if body, c.err = io.ReadAll(res.Body); c.err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
res.Body = io.NopCloser(bytes.NewReader(body))
|
||||
|
||||
if res.Proto != ProtoEvent {
|
||||
c.res <- res
|
||||
} else if c.OnEvent != nil {
|
||||
c.OnEvent(res)
|
||||
}
|
||||
}
|
||||
|
||||
close(c.res)
|
||||
}
|
||||
|
||||
func (c *Client) GetAccessories() ([]*Accessory, error) {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package hap
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -22,6 +23,9 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
||||
if err := req.Write(c.Conn); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.res != nil {
|
||||
return <-c.res, c.err
|
||||
}
|
||||
return http.ReadResponse(c.reader, req)
|
||||
}
|
||||
|
||||
@@ -54,3 +58,27 @@ func (c *Client) Post(path, contentType string, body io.Reader) (*http.Response,
|
||||
func (c *Client) Put(path, contentType string, body io.Reader) (*http.Response, error) {
|
||||
return c.Request("PUT", path, contentType, body)
|
||||
}
|
||||
|
||||
const ProtoEvent = "EVENT/1.0"
|
||||
|
||||
func ReadResponse(r *bufio.Reader, req *http.Request) (*http.Response, error) {
|
||||
b, err := r.Peek(9)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if string(b) != ProtoEvent {
|
||||
return http.ReadResponse(r, req)
|
||||
}
|
||||
|
||||
copy(b, "HTTP/1.1 ")
|
||||
|
||||
res, err := http.ReadResponse(r, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res.Proto = ProtoEvent
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
@@ -1,68 +0,0 @@
|
||||
package hap
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type EventReader struct {
|
||||
r io.Reader
|
||||
ch chan []byte
|
||||
err error
|
||||
left []byte
|
||||
}
|
||||
|
||||
func NewEventReader(r io.Reader) *EventReader {
|
||||
e := &EventReader{r: r, ch: make(chan []byte, 1)}
|
||||
go e.background()
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *EventReader) background() {
|
||||
b := make([]byte, 32*1024)
|
||||
for {
|
||||
n, err := e.r.Read(b)
|
||||
if err != nil {
|
||||
e.err = err
|
||||
return
|
||||
}
|
||||
|
||||
if n >= 6 && string(b[:6]) == "EVENT " {
|
||||
panic("TODO")
|
||||
}
|
||||
|
||||
// copy because will be overwriten
|
||||
buf := make([]byte, n)
|
||||
copy(buf, b)
|
||||
e.ch <- buf
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EventReader) Read(p []byte) (n int, err error) {
|
||||
if e.err != nil {
|
||||
return 0, e.err
|
||||
}
|
||||
|
||||
// if something left after previous reading
|
||||
if e.left != nil {
|
||||
// if still something left
|
||||
if n = copy(p, e.left); n < len(e.left) {
|
||||
e.left = e.left[n:]
|
||||
} else {
|
||||
e.left = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-time.After(time.Second * 5):
|
||||
return 0, os.ErrDeadlineExceeded
|
||||
case b := <-e.ch:
|
||||
if n = copy(p, b); n < len(b) {
|
||||
e.left = b[n:]
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
@@ -66,7 +66,8 @@ type JSONCharacters struct {
|
||||
type JSONCharacter struct {
|
||||
AID uint8 `json:"aid"`
|
||||
IID uint64 `json:"iid"`
|
||||
Value any `json:"value"`
|
||||
Value any `json:"value,omitempty"`
|
||||
Event any `json:"ev,omitempty"`
|
||||
}
|
||||
|
||||
func SanitizePin(pin string) (string, error) {
|
||||
|
@@ -1,7 +1,9 @@
|
||||
package secure
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
@@ -14,6 +16,9 @@ import (
|
||||
type Conn struct {
|
||||
conn net.Conn
|
||||
|
||||
rd *bufio.Reader
|
||||
wr *bufio.Writer
|
||||
|
||||
encryptKey []byte
|
||||
decryptKey []byte
|
||||
encryptCnt uint64
|
||||
@@ -33,11 +38,19 @@ func Client(conn net.Conn, sharedKey []byte, isClient bool) (net.Conn, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isClient {
|
||||
return &Conn{conn: conn, encryptKey: key2, decryptKey: key1}, nil
|
||||
} else {
|
||||
return &Conn{conn: conn, encryptKey: key1, decryptKey: key2}, nil
|
||||
c := &Conn{
|
||||
conn: conn,
|
||||
rd: bufio.NewReaderSize(conn, 32*1024),
|
||||
wr: bufio.NewWriterSize(conn, 32*1024),
|
||||
}
|
||||
|
||||
if isClient {
|
||||
c.encryptKey, c.decryptKey = key2, key1
|
||||
} else {
|
||||
c.encryptKey, c.decryptKey = key1, key2
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -50,84 +63,63 @@ const (
|
||||
)
|
||||
|
||||
func (c *Conn) Read(b []byte) (n int, err error) {
|
||||
verify := make([]byte, VerifySize) // = packet length
|
||||
buf := make([]byte, PacketSizeMax+Overhead)
|
||||
nonce := make([]byte, NonceSize)
|
||||
|
||||
for {
|
||||
if len(b) < PacketSizeMax {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = io.ReadFull(c.conn, verify); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
size := binary.LittleEndian.Uint16(verify)
|
||||
ciphertext := buf[:size+Overhead]
|
||||
|
||||
if _, err = io.ReadFull(c.conn, ciphertext); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
binary.LittleEndian.PutUint64(nonce, c.decryptCnt)
|
||||
c.decryptCnt++
|
||||
|
||||
// put decrypted text to b's end
|
||||
_, err = chacha20poly1305.DecryptAndVerify(c.decryptKey, b[:0], nonce, ciphertext, verify)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
n += int(size) // plaintext size
|
||||
|
||||
// Finish when all bytes fit in b
|
||||
if size < PacketSizeMax {
|
||||
return
|
||||
}
|
||||
|
||||
b = b[size:]
|
||||
if cap(b) < PacketSizeMax {
|
||||
return 0, errors.New("hap: read buffer is too small")
|
||||
}
|
||||
|
||||
verify := make([]byte, 2) // verify = plain message size
|
||||
if _, err = io.ReadFull(c.rd, verify); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
n = int(binary.LittleEndian.Uint16(verify))
|
||||
ciphertext := make([]byte, n+Overhead)
|
||||
|
||||
if _, err = io.ReadFull(c.rd, ciphertext); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
nonce := make([]byte, NonceSize)
|
||||
binary.LittleEndian.PutUint64(nonce, c.decryptCnt)
|
||||
c.decryptCnt++
|
||||
|
||||
_, err = chacha20poly1305.DecryptAndVerify(c.decryptKey, b[:0], nonce, ciphertext, verify)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Conn) Write(b []byte) (n int, err error) {
|
||||
c.mx.Lock()
|
||||
defer c.mx.Unlock()
|
||||
|
||||
buf := make([]byte, 0, PacketSizeMax+Overhead)
|
||||
nonce := make([]byte, NonceSize)
|
||||
buf := make([]byte, NonceSize+PacketSizeMax+Overhead)
|
||||
verify := buf[:VerifySize] // part of write buffer
|
||||
verify := make([]byte, VerifySize)
|
||||
|
||||
for {
|
||||
for len(b) > 0 {
|
||||
size := len(b)
|
||||
if size > PacketSizeMax {
|
||||
size = PacketSizeMax
|
||||
}
|
||||
|
||||
binary.LittleEndian.PutUint16(verify, uint16(size))
|
||||
if _, err = c.wr.Write(verify); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
binary.LittleEndian.PutUint64(nonce, c.encryptCnt)
|
||||
c.encryptCnt++
|
||||
|
||||
// put encrypted text to writing buffer just after size (2 bytes)
|
||||
_, err = chacha20poly1305.EncryptAndSeal(c.encryptKey, buf[2:2], nonce, b[:size], verify)
|
||||
_, err = chacha20poly1305.EncryptAndSeal(c.encryptKey, buf, nonce, b[:size], verify)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = c.conn.Write(buf[:VerifySize+size+Overhead]); err != nil {
|
||||
if _, err = c.wr.Write(buf[:size+Overhead]); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
n += size // plaintext size
|
||||
|
||||
if size < PacketSizeMax {
|
||||
break
|
||||
}
|
||||
|
||||
b = b[PacketSizeMax:]
|
||||
b = b[size:]
|
||||
n += size
|
||||
}
|
||||
|
||||
err = c.wr.Flush()
|
||||
return
|
||||
}
|
||||
|
||||
|
@@ -146,7 +146,7 @@ func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Co
|
||||
|
||||
clientPublic := s.GetPair(conn, plainM3.Identifier)
|
||||
if clientPublic == nil {
|
||||
return fmt.Errorf("hap: PairVerify from: %s, with unknown client_id: %s", plainM3.Identifier)
|
||||
return fmt.Errorf("hap: PairVerify from: %s, with unknown client_id: %s", conn.RemoteAddr(), plainM3.Identifier)
|
||||
}
|
||||
|
||||
b = Append(plainM1.PublicKey, plainM3.Identifier, sessionPublic)
|
||||
|
@@ -64,20 +64,22 @@ func (c *Producer) Start() error {
|
||||
|
||||
buf = append(buf, b[:n]...)
|
||||
|
||||
i := annexb.IndexFrame(buf)
|
||||
if i < 0 {
|
||||
continue
|
||||
for {
|
||||
i := annexb.IndexFrame(buf)
|
||||
if i < 0 {
|
||||
break
|
||||
}
|
||||
|
||||
pkt := &rtp.Packet{
|
||||
Header: rtp.Header{Timestamp: core.Now90000()},
|
||||
Payload: annexb.EncodeToAVCC(buf[:i], true),
|
||||
}
|
||||
c.Receivers[0].WriteRTP(pkt)
|
||||
|
||||
//log.Printf("[AVC] %v, len: %d", h264.Types(pkt.Payload), len(pkt.Payload))
|
||||
|
||||
buf = buf[i:]
|
||||
}
|
||||
|
||||
pkt := &rtp.Packet{
|
||||
Header: rtp.Header{Timestamp: core.Now90000()},
|
||||
Payload: annexb.EncodeToAVCC(buf[:i], true),
|
||||
}
|
||||
c.Receivers[0].WriteRTP(pkt)
|
||||
|
||||
//log.Printf("[AVC] %v, len: %d", h264.Types(pkt.Payload), len(pkt.Payload))
|
||||
|
||||
buf = buf[i:]
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -52,6 +52,8 @@ func (k *Keyframe) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv
|
||||
|
||||
if track.Codec.IsRTP() {
|
||||
sender.Handler = h264.RTPDepay(track.Codec, sender.Handler)
|
||||
} else {
|
||||
sender.Handler = h264.RepairAVCC(track.Codec, sender.Handler)
|
||||
}
|
||||
|
||||
case core.CodecH265:
|
||||
@@ -66,9 +68,7 @@ func (k *Keyframe) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv
|
||||
}
|
||||
|
||||
if track.Codec.IsRTP() {
|
||||
sender.Handler = h264.RTPDepay(track.Codec, sender.Handler)
|
||||
} else {
|
||||
sender.Handler = h264.RepairAVCC(track.Codec, sender.Handler)
|
||||
sender.Handler = h265.RTPDepay(track.Codec, sender.Handler)
|
||||
}
|
||||
|
||||
case core.CodecJPEG:
|
||||
|
24
pkg/mdns/syscall_darwin.go
Normal file
24
pkg/mdns/syscall_darwin.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package mdns
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func SetsockoptInt(fd uintptr, level, opt int, value int) (err error) {
|
||||
// change SO_REUSEADDR and REUSEPORT flags simultaneously for BSD-like OS
|
||||
// https://github.com/AlexxIT/go2rtc/issues/626
|
||||
// https://stackoverflow.com/questions/14388706/how-do-so-reuseaddr-and-so-reuseport-differ/14388707
|
||||
if opt == syscall.SO_REUSEADDR {
|
||||
if err = syscall.SetsockoptInt(int(fd), level, opt, value); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
opt = syscall.SO_REUSEPORT
|
||||
}
|
||||
|
||||
return syscall.SetsockoptInt(int(fd), level, opt, value)
|
||||
}
|
||||
|
||||
func SetsockoptIPMreq(fd uintptr, level, opt int, mreq *syscall.IPMreq) (err error) {
|
||||
return syscall.SetsockoptIPMreq(int(fd), level, opt, mreq)
|
||||
}
|
@@ -1,8 +1,8 @@
|
||||
//go:build darwin || linux
|
||||
|
||||
package mdns
|
||||
|
||||
import "syscall"
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func SetsockoptInt(fd uintptr, level, opt int, value int) (err error) {
|
||||
return syscall.SetsockoptInt(int(fd), level, opt, value)
|
@@ -2,7 +2,6 @@ package mp4
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
@@ -43,13 +42,17 @@ func (m *Muxer) GetInit() ([]byte, error) {
|
||||
pps = []byte{0x68, 0xce, 0x38, 0x80}
|
||||
}
|
||||
|
||||
s := h264.DecodeSPS(sps)
|
||||
if s == nil {
|
||||
return nil, errors.New("mp4: can't parse SPS")
|
||||
var width, height uint16
|
||||
if s := h264.DecodeSPS(sps); s != nil {
|
||||
width = s.Width()
|
||||
height = s.Height()
|
||||
} else {
|
||||
width = 1920
|
||||
height = 1080
|
||||
}
|
||||
|
||||
mv.WriteVideoTrack(
|
||||
uint32(i+1), codec.Name, codec.ClockRate, s.Width(), s.Height(), h264.EncodeConfig(sps, pps),
|
||||
uint32(i+1), codec.Name, codec.ClockRate, width, height, h264.EncodeConfig(sps, pps),
|
||||
)
|
||||
|
||||
case core.CodecH265:
|
||||
@@ -65,13 +68,17 @@ func (m *Muxer) GetInit() ([]byte, error) {
|
||||
pps = []byte{0x44, 0x01, 0xc0, 0x73, 0xc0, 0x4c, 0x90}
|
||||
}
|
||||
|
||||
s := h265.DecodeSPS(sps)
|
||||
if s == nil {
|
||||
return nil, errors.New("mp4: can't parse SPS")
|
||||
var width, height uint16
|
||||
if s := h265.DecodeSPS(sps); s != nil {
|
||||
width = s.Width()
|
||||
height = s.Height()
|
||||
} else {
|
||||
width = 1920
|
||||
height = 1080
|
||||
}
|
||||
|
||||
mv.WriteVideoTrack(
|
||||
uint32(i+1), codec.Name, codec.ClockRate, s.Width(), s.Height(), h265.EncodeConfig(vps, sps, pps),
|
||||
uint32(i+1), codec.Name, codec.ClockRate, width, height, h265.EncodeConfig(vps, sps, pps),
|
||||
)
|
||||
|
||||
case core.CodecAAC:
|
||||
|
@@ -5,7 +5,6 @@ import (
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"html"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -13,6 +12,8 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
const PathDevice = "/onvif/device_service"
|
||||
@@ -78,10 +79,10 @@ func (c *Client) GetURI() (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
uri := FindTagValue(b, "Uri")
|
||||
uri = html.UnescapeString(uri)
|
||||
rawURL := FindTagValue(b, "Uri")
|
||||
rawURL = strings.TrimSpace(html.UnescapeString(rawURL))
|
||||
|
||||
u, err := url.Parse(uri)
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@@ -1,16 +1,17 @@
|
||||
package onvif
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"net"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
func FindTagValue(b []byte, tag string) string {
|
||||
re := regexp.MustCompile(`<[^/>]*` + tag + `[^>]*>([^<]+)`)
|
||||
re := regexp.MustCompile(`(?s)[:<]` + tag + `>([^<]+)`)
|
||||
m := re.FindSubmatch(b)
|
||||
if len(m) != 2 {
|
||||
return ""
|
||||
|
199
pkg/onvif/onvif_test.go
Normal file
199
pkg/onvif/onvif_test.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package onvif
|
||||
|
||||
import (
|
||||
"html"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetStreamUri(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
xml string
|
||||
url string
|
||||
}{
|
||||
{
|
||||
name: "Dahua stream default",
|
||||
xml: `<?xml version="1.0" encoding="utf-8" standalone="yes" ?><s:Envelope xmlns:sc="http://www.w3.org/2003/05/soap-encoding" xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tt="http://www.onvif.org/ver10/schema" xmlns:trt="http://www.onvif.org/ver10/media/wsdl"><s:Header/><s:Body><trt:GetStreamUriResponse><trt:MediaUri><tt:Uri>rtsp://192.168.1.123:554/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif</tt:Uri><tt:InvalidAfterConnect>true</tt:InvalidAfterConnect><tt:InvalidAfterReboot>true</tt:InvalidAfterReboot><tt:Timeout>PT0S</tt:Timeout></trt:MediaUri></trt:GetStreamUriResponse></s:Body></s:Envelope>`,
|
||||
url: "rtsp://192.168.1.123:554/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif",
|
||||
},
|
||||
{
|
||||
name: "Dahua snapshot default",
|
||||
xml: `<?xml version="1.0" encoding="utf-8" standalone="yes" ?><s:Envelope xmlns:sc="http://www.w3.org/2003/05/soap-encoding" xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tt="http://www.onvif.org/ver10/schema" xmlns:trt="http://www.onvif.org/ver10/media/wsdl"><s:Header/><s:Body><trt:GetSnapshotUriResponse><trt:MediaUri><tt:Uri>http://192.168.1.123/onvifsnapshot/media_service/snapshot?channel=1&subtype=1</tt:Uri><tt:InvalidAfterConnect>false</tt:InvalidAfterConnect><tt:InvalidAfterReboot>false</tt:InvalidAfterReboot><tt:Timeout>PT0S</tt:Timeout></trt:MediaUri></trt:GetSnapshotUriResponse></s:Body></s:Envelope>`,
|
||||
url: "http://192.168.1.123/onvifsnapshot/media_service/snapshot?channel=1&subtype=1",
|
||||
},
|
||||
{
|
||||
name: "Dahua stream formatted",
|
||||
xml: `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<s:Envelope xmlns:sc="http://www.w3.org/2003/05/soap-encoding"
|
||||
xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tt="http://www.onvif.org/ver10/schema"
|
||||
xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
|
||||
<s:Header />
|
||||
<s:Body>
|
||||
<trt:GetStreamUriResponse>
|
||||
<trt:MediaUri>
|
||||
<tt:Uri>
|
||||
rtsp://192.168.1.123:554/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif</tt:Uri>
|
||||
<tt:InvalidAfterConnect>true</tt:InvalidAfterConnect>
|
||||
<tt:InvalidAfterReboot>true</tt:InvalidAfterReboot>
|
||||
<tt:Timeout>PT0S</tt:Timeout>
|
||||
</trt:MediaUri>
|
||||
</trt:GetStreamUriResponse>
|
||||
</s:Body>
|
||||
</s:Envelope>`,
|
||||
url: "rtsp://192.168.1.123:554/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif",
|
||||
},
|
||||
{
|
||||
name: "Dahua snapshot formatted",
|
||||
xml: `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<s:Envelope xmlns:sc="http://www.w3.org/2003/05/soap-encoding"
|
||||
xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tt="http://www.onvif.org/ver10/schema"
|
||||
xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
|
||||
<s:Header />
|
||||
<s:Body>
|
||||
<trt:GetSnapshotUriResponse>
|
||||
<trt:MediaUri>
|
||||
<tt:Uri>
|
||||
http://192.168.1.123/onvifsnapshot/media_service/snapshot?channel=1&subtype=1</tt:Uri>
|
||||
<tt:InvalidAfterConnect>false</tt:InvalidAfterConnect>
|
||||
<tt:InvalidAfterReboot>false</tt:InvalidAfterReboot>
|
||||
<tt:Timeout>PT0S</tt:Timeout>
|
||||
</trt:MediaUri>
|
||||
</trt:GetSnapshotUriResponse>
|
||||
</s:Body>
|
||||
</s:Envelope>`,
|
||||
url: "http://192.168.1.123/onvifsnapshot/media_service/snapshot?channel=1&subtype=1",
|
||||
},
|
||||
{
|
||||
name: "Unknown",
|
||||
xml: `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<SOAP-ENV:Envelope ...>
|
||||
<SOAP-ENV:Header></SOAP-ENV:Header>
|
||||
<SOAP-ENV:Body>
|
||||
<MC1:GetStreamUriResponse>
|
||||
<MC1:MediaUri>
|
||||
<MC2:Uri>
|
||||
rtsp://192.168.5.53:8090/profile1=r
|
||||
</MC2:Uri>
|
||||
</MC1:MediaUri>
|
||||
</MC1:GetStreamUriResponse>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`,
|
||||
url: "rtsp://192.168.5.53:8090/profile1=r",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
uri := FindTagValue([]byte(test.xml), "Uri")
|
||||
uri = strings.TrimSpace(html.UnescapeString(uri))
|
||||
u, err := url.Parse(uri)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, test.url, u.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCapabilities(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
xml string
|
||||
}{
|
||||
{
|
||||
name: "Dahua default",
|
||||
xml: `<?xml version="1.0" encoding="utf-8" standalone="yes" ?><s:Envelope xmlns:sc="http://www.w3.org/2003/05/soap-encoding" xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tt="http://www.onvif.org/ver10/schema" xmlns:tds="http://www.onvif.org/ver10/device/wsdl"><s:Header/><s:Body><tds:GetCapabilitiesResponse><tds:Capabilities><tt:Analytics><tt:XAddr>http://192.168.1.123/onvif/analytics_service</tt:XAddr><tt:RuleSupport>true</tt:RuleSupport><tt:AnalyticsModuleSupport>true</tt:AnalyticsModuleSupport></tt:Analytics><tt:Device><tt:XAddr>http://192.168.1.123/onvif/device_service</tt:XAddr><tt:Network><tt:IPFilter>false</tt:IPFilter><tt:ZeroConfiguration>false</tt:ZeroConfiguration><tt:IPVersion6>false</tt:IPVersion6><tt:DynDNS>false</tt:DynDNS><tt:Extension><tt:Dot11Configuration>false</tt:Dot11Configuration></tt:Extension></tt:Network><tt:System><tt:DiscoveryResolve>false</tt:DiscoveryResolve><tt:DiscoveryBye>true</tt:DiscoveryBye><tt:RemoteDiscovery>false</tt:RemoteDiscovery><tt:SystemBackup>false</tt:SystemBackup><tt:SystemLogging>true</tt:SystemLogging><tt:FirmwareUpgrade>true</tt:FirmwareUpgrade><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>00</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>10</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>20</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>30</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>40</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>42</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>16</tt:Major><tt:Minor>12</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>18</tt:Major><tt:Minor>06</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>18</tt:Major><tt:Minor>12</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>19</tt:Major><tt:Minor>06</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>19</tt:Major><tt:Minor>12</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>20</tt:Major><tt:Minor>06</tt:Minor></tt:SupportedVersions><tt:Extension><tt:HttpFirmwareUpgrade>true</tt:HttpFirmwareUpgrade><tt:HttpSystemBackup>false</tt:HttpSystemBackup><tt:HttpSystemLogging>false</tt:HttpSystemLogging><tt:HttpSupportInformation>false</tt:HttpSupportInformation></tt:Extension></tt:System><tt:IO><tt:InputConnectors>2</tt:InputConnectors><tt:RelayOutputs>1</tt:RelayOutputs><tt:Extension><tt:Auxiliary>false</tt:Auxiliary><tt:AuxiliaryCommands></tt:AuxiliaryCommands><tt:Extension></tt:Extension></tt:Extension></tt:IO><tt:Security><tt:TLS1.1>false</tt:TLS1.1><tt:TLS1.2>false</tt:TLS1.2><tt:OnboardKeyGeneration>false</tt:OnboardKeyGeneration><tt:AccessPolicyConfig>false</tt:AccessPolicyConfig><tt:X.509Token>false</tt:X.509Token><tt:SAMLToken>false</tt:SAMLToken><tt:KerberosToken>false</tt:KerberosToken><tt:RELToken>false</tt:RELToken><tt:Extension><tt:TLS1.0>false</tt:TLS1.0><tt:Extension><tt:Dot1X>false</tt:Dot1X><tt:SupportedEAPMethod>0</tt:SupportedEAPMethod><tt:RemoteUserHandling>false</tt:RemoteUserHandling></tt:Extension></tt:Extension></tt:Security></tt:Device><tt:Events><tt:XAddr>http://192.168.1.123/onvif/event_service</tt:XAddr><tt:WSSubscriptionPolicySupport>true</tt:WSSubscriptionPolicySupport><tt:WSPullPointSupport>true</tt:WSPullPointSupport><tt:WSPausableSubscriptionManagerInterfaceSupport>false</tt:WSPausableSubscriptionManagerInterfaceSupport></tt:Events><tt:Imaging><tt:XAddr>http://192.168.1.123/onvif/imaging_service</tt:XAddr></tt:Imaging><tt:Media><tt:XAddr>http://192.168.1.123/onvif/media_service</tt:XAddr><tt:StreamingCapabilities><tt:RTPMulticast>true</tt:RTPMulticast><tt:RTP_TCP>true</tt:RTP_TCP><tt:RTP_RTSP_TCP>true</tt:RTP_RTSP_TCP></tt:StreamingCapabilities><tt:Extension><tt:ProfileCapabilities><tt:MaximumNumberOfProfiles>6</tt:MaximumNumberOfProfiles></tt:ProfileCapabilities></tt:Extension></tt:Media><tt:Extension><tt:DeviceIO><tt:XAddr>http://192.168.1.123/onvif/deviceIO_service</tt:XAddr><tt:VideoSources>1</tt:VideoSources><tt:VideoOutputs>0</tt:VideoOutputs><tt:AudioSources>1</tt:AudioSources><tt:AudioOutputs>1</tt:AudioOutputs><tt:RelayOutputs>1</tt:RelayOutputs></tt:DeviceIO></tt:Extension></tds:Capabilities></tds:GetCapabilitiesResponse></s:Body></s:Envelope>`,
|
||||
},
|
||||
{
|
||||
name: "Dahua formatted",
|
||||
xml: `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<s:Envelope xmlns:sc="http://www.w3.org/2003/05/soap-encoding"
|
||||
xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tt="http://www.onvif.org/ver10/schema"
|
||||
xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
|
||||
<s:Header />
|
||||
<s:Body>
|
||||
<tds:GetCapabilitiesResponse>
|
||||
<tds:Capabilities>
|
||||
<tt:Analytics>
|
||||
<tt:XAddr>http://192.168.1.123/onvif/analytics_service</tt:XAddr>
|
||||
<tt:RuleSupport>true</tt:RuleSupport>
|
||||
<tt:AnalyticsModuleSupport>true</tt:AnalyticsModuleSupport>
|
||||
</tt:Analytics>
|
||||
<tt:Device>
|
||||
<tt:XAddr>http://192.168.1.123/onvif/device_service</tt:XAddr>
|
||||
<tt:Network>
|
||||
<tt:IPFilter>false</tt:IPFilter>
|
||||
<tt:ZeroConfiguration>false</tt:ZeroConfiguration>
|
||||
<tt:IPVersion6>false</tt:IPVersion6>
|
||||
<tt:DynDNS>false</tt:DynDNS>
|
||||
<tt:Extension>
|
||||
<tt:Dot11Configuration>false</tt:Dot11Configuration>
|
||||
</tt:Extension>
|
||||
</tt:Network>
|
||||
<tt:System>
|
||||
...
|
||||
</tt:System>
|
||||
<tt:IO>
|
||||
<tt:InputConnectors>2</tt:InputConnectors>
|
||||
<tt:RelayOutputs>1</tt:RelayOutputs>
|
||||
<tt:Extension>
|
||||
<tt:Auxiliary>false</tt:Auxiliary>
|
||||
<tt:AuxiliaryCommands></tt:AuxiliaryCommands>
|
||||
<tt:Extension></tt:Extension>
|
||||
</tt:Extension>
|
||||
</tt:IO>
|
||||
<tt:Security>
|
||||
...
|
||||
</tt:Security>
|
||||
</tt:Device>
|
||||
<tt:Events>
|
||||
<tt:XAddr>http://192.168.1.123/onvif/event_service</tt:XAddr>
|
||||
<tt:WSSubscriptionPolicySupport>true</tt:WSSubscriptionPolicySupport>
|
||||
<tt:WSPullPointSupport>true</tt:WSPullPointSupport>
|
||||
<tt:WSPausableSubscriptionManagerInterfaceSupport>false</tt:WSPausableSubscriptionManagerInterfaceSupport>
|
||||
</tt:Events>
|
||||
<tt:Imaging>
|
||||
<tt:XAddr>http://192.168.1.123/onvif/imaging_service</tt:XAddr>
|
||||
</tt:Imaging>
|
||||
<tt:Media>
|
||||
<tt:XAddr>http://192.168.1.123/onvif/media_service</tt:XAddr>
|
||||
<tt:StreamingCapabilities>
|
||||
<tt:RTPMulticast>true</tt:RTPMulticast>
|
||||
<tt:RTP_TCP>true</tt:RTP_TCP>
|
||||
<tt:RTP_RTSP_TCP>true</tt:RTP_RTSP_TCP>
|
||||
</tt:StreamingCapabilities>
|
||||
<tt:Extension>
|
||||
<tt:ProfileCapabilities>
|
||||
<tt:MaximumNumberOfProfiles>6</tt:MaximumNumberOfProfiles>
|
||||
</tt:ProfileCapabilities>
|
||||
</tt:Extension>
|
||||
</tt:Media>
|
||||
<tt:Extension>
|
||||
<tt:DeviceIO>
|
||||
<tt:XAddr>http://192.168.1.123/onvif/deviceIO_service</tt:XAddr>
|
||||
<tt:VideoSources>1</tt:VideoSources>
|
||||
<tt:VideoOutputs>0</tt:VideoOutputs>
|
||||
<tt:AudioSources>1</tt:AudioSources>
|
||||
<tt:AudioOutputs>1</tt:AudioOutputs>
|
||||
<tt:RelayOutputs>1</tt:RelayOutputs>
|
||||
</tt:DeviceIO>
|
||||
</tt:Extension>
|
||||
</tds:Capabilities>
|
||||
</tds:GetCapabilitiesResponse>
|
||||
</s:Body>
|
||||
</s:Envelope>`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
rawURL := FindTagValue([]byte(test.xml), "Media.+?XAddr")
|
||||
require.Equal(t, "http://192.168.1.123/onvif/media_service", rawURL)
|
||||
|
||||
rawURL = FindTagValue([]byte(test.xml), "Imaging.+?XAddr")
|
||||
require.Equal(t, "http://192.168.1.123/onvif/imaging_service", rawURL)
|
||||
})
|
||||
}
|
||||
}
|
@@ -6,16 +6,17 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/roborock/iot"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
"log"
|
||||
"net/rpc"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/roborock/iot"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
@@ -38,13 +39,15 @@ func NewClient(url string) *Client {
|
||||
return &Client{url: url}
|
||||
}
|
||||
|
||||
func (c *Client) Dial() (err error) {
|
||||
func (c *Client) Dial() error {
|
||||
u, err := url.Parse(c.url)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.iot, err = iot.Dial(c.url)
|
||||
if c.iot, err = iot.Dial(c.url); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.pin = u.Query().Get("pin")
|
||||
if c.pin != "" {
|
||||
|
19
pkg/rtmp/README.md
Normal file
19
pkg/rtmp/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
## Logs
|
||||
|
||||
```
|
||||
request []interface {}{"connect", 1, map[string]interface {}{"app":"s", "flashVer":"FMLE/3.0 (compatible; FMSc/1.0)", "tcUrl":"rtmps://xxx.rtmp.t.me/s/xxxxx"}}
|
||||
response []interface {}{"_result", 1, map[string]interface {}{"capabilities":31, "fmsVer":"FMS/3,0,1,123"}, map[string]interface {}{"code":"NetConnection.Connect.Success", "description":"Connection succeeded.", "level":"status", "objectEncoding":0}}
|
||||
request []interface {}{"releaseStream", 2, interface {}(nil), "xxxxx"}
|
||||
request []interface {}{"FCPublish", 3, interface {}(nil), "xxxxx"}
|
||||
request []interface {}{"createStream", 4, interface {}(nil)}
|
||||
response []interface {}{"_result", 2, interface {}(nil)}
|
||||
response []interface {}{"_result", 4, interface {}(nil), 1}
|
||||
request []interface {}{"publish", 5, interface {}(nil), "xxxxx", "live"}
|
||||
response []interface {}{"onStatus", 0, interface {}(nil), map[string]interface {}{"code":"NetStream.Publish.Start", "description":"xxxxx is now published", "detail":"xxxxx", "level":"status"}}
|
||||
```
|
||||
|
||||
## Useful links
|
||||
|
||||
- https://en.wikipedia.org/wiki/Flash_Video
|
||||
- https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol
|
||||
- https://rtmp.veriskope.com/pdf/rtmp_specification_1.0.pdf
|
155
pkg/rtmp/client.go
Normal file
155
pkg/rtmp/client.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package rtmp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
)
|
||||
|
||||
func DialPlay(rawURL string) (core.Producer, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn, err := tcp.Dial(u, core.ConnDialTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rtmpConn, err := NewClient(conn, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = rtmpConn.play(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rtmpConn.Producer()
|
||||
}
|
||||
|
||||
func DialPublish(rawURL string) (io.Writer, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn, err := tcp.Dial(u, core.ConnDialTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := NewClient(conn, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = client.publish(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func NewClient(conn net.Conn, u *url.URL) (*Conn, error) {
|
||||
c := &Conn{
|
||||
url: u.String(),
|
||||
|
||||
conn: conn,
|
||||
rd: bufio.NewReaderSize(conn, core.BufferSize),
|
||||
wr: conn,
|
||||
|
||||
chunks: map[uint8]*header{},
|
||||
|
||||
rdPacketSize: 128,
|
||||
wrPacketSize: 4096, // OBS - 4096, Reolink - 4096
|
||||
}
|
||||
|
||||
if args := strings.Split(u.Path, "/"); len(args) >= 2 {
|
||||
c.App = args[1]
|
||||
if len(args) >= 3 {
|
||||
c.Stream = args[2]
|
||||
if u.RawQuery != "" {
|
||||
c.Stream += "?" + u.RawQuery
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.clienHandshake(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.writePacketSize(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Conn) clienHandshake() error {
|
||||
// simple handshake without real random and check response
|
||||
b := make([]byte, 1+1536)
|
||||
b[0] = 0x03
|
||||
// write C0+C1
|
||||
if _, err := c.conn.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
// read S0+S1
|
||||
if _, err := io.ReadFull(c.rd, b); err != nil {
|
||||
return err
|
||||
}
|
||||
// write S1
|
||||
if _, err := c.conn.Write(b[1:]); err != nil {
|
||||
return err
|
||||
}
|
||||
// read C1, skip check
|
||||
if _, err := io.ReadFull(c.rd, b[1:]); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) play() error {
|
||||
if err := c.writeConnect(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.writeCreateStream(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.writePlay(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) publish() error {
|
||||
if err := c.writeConnect(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.writeReleaseStream(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.writeCreateStream(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.writePublish(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
_, _, _, err := c.readMessage()
|
||||
//log.Printf("!!! %d %d %.30x", msgType, timeMS, b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
353
pkg/rtmp/conn.go
Normal file
353
pkg/rtmp/conn.go
Normal file
@@ -0,0 +1,353 @@
|
||||
package rtmp
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/flv/amf"
|
||||
)
|
||||
|
||||
const (
|
||||
TypeSetPacketSize = 1
|
||||
TypeServerBandwidth = 5
|
||||
TypeClientBandwidth = 6
|
||||
TypeAudio = 8
|
||||
TypeVideo = 9
|
||||
TypeData = 18
|
||||
TypeCommand = 20
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
App string
|
||||
Stream string
|
||||
Intent string
|
||||
|
||||
rdPacketSize uint32
|
||||
wrPacketSize uint32
|
||||
|
||||
chunks map[byte]*header
|
||||
streamID byte
|
||||
url string
|
||||
|
||||
conn net.Conn
|
||||
rd io.Reader
|
||||
wr io.Writer
|
||||
|
||||
rdBuf []byte
|
||||
wrBuf []byte
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func (c *Conn) readResponse(transID float64) ([]any, error) {
|
||||
for {
|
||||
msgType, _, b, err := c.readMessage()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch msgType {
|
||||
case TypeSetPacketSize:
|
||||
c.rdPacketSize = binary.BigEndian.Uint32(b)
|
||||
case TypeCommand:
|
||||
items, _ := amf.NewReader(b).ReadItems()
|
||||
if len(items) >= 3 && items[1] == transID {
|
||||
return items, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type header struct {
|
||||
timeMS uint32
|
||||
dataSize uint32
|
||||
tagType byte
|
||||
streamID uint32
|
||||
}
|
||||
|
||||
//var ErrNotImplemented = errors.New("rtmp: not implemented")
|
||||
|
||||
func (c *Conn) readMessage() (byte, uint32, []byte, error) {
|
||||
b, err := c.readSize(1) // doesn't support big chunkID!!!
|
||||
if err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
|
||||
hdrType := b[0] >> 6
|
||||
chunkID := b[0] & 0b111111
|
||||
|
||||
// storing header information for support header type 3
|
||||
hdr, ok := c.chunks[chunkID]
|
||||
if !ok {
|
||||
hdr = &header{}
|
||||
c.chunks[chunkID] = hdr
|
||||
}
|
||||
|
||||
switch hdrType {
|
||||
case 0: // 12 byte header (full header)
|
||||
if b, err = c.readSize(11); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
_ = b[7]
|
||||
hdr.timeMS = Uint24(b)
|
||||
hdr.dataSize = Uint24(b[3:])
|
||||
hdr.tagType = b[6]
|
||||
hdr.streamID = binary.LittleEndian.Uint32(b[7:])
|
||||
|
||||
case 1: // 8 bytes - like type b00, not including message ID (4 last bytes)
|
||||
if b, err = c.readSize(7); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
_ = b[6]
|
||||
hdr.timeMS = Uint24(b) // timestamp
|
||||
hdr.dataSize = Uint24(b[3:]) // msgdatalen
|
||||
hdr.tagType = b[6] // msgtypeid
|
||||
|
||||
case 2: // 4 bytes - Basic Header and timestamp (3 bytes) are included
|
||||
if b, err = c.readSize(3); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
hdr.timeMS = Uint24(b) // timestamp
|
||||
|
||||
case 3: // 1 byte - only the Basic Header is included
|
||||
// use here hdr from previous msg with same session ID (sid)
|
||||
}
|
||||
|
||||
timeMS := hdr.timeMS
|
||||
if timeMS == 0xFFFFFF {
|
||||
if b, err = c.readSize(4); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
timeMS = binary.BigEndian.Uint32(b)
|
||||
}
|
||||
|
||||
//log.Printf("[rtmp] hdr=%d chunkID=%d timeMS=%d size=%d tagType=%d streamID=%d", hdrType, chunkID, hdr.timeMS, hdr.dataSize, hdr.tagType, hdr.streamID)
|
||||
|
||||
// 1. Response zero size
|
||||
if hdr.dataSize == 0 {
|
||||
return hdr.tagType, timeMS, nil, nil
|
||||
}
|
||||
|
||||
b = make([]byte, hdr.dataSize)
|
||||
|
||||
// 2. Response small packet
|
||||
if hdr.dataSize <= c.rdPacketSize {
|
||||
if _, err = io.ReadFull(c.rd, b); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
return hdr.tagType, timeMS, b, nil
|
||||
}
|
||||
|
||||
// 3. Response big packet
|
||||
var i0 uint32
|
||||
for i1 := c.rdPacketSize; i1 < hdr.dataSize; i1 += c.rdPacketSize {
|
||||
if _, err = io.ReadFull(c.rd, b[i0:i1]); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
|
||||
if _, err = c.readSize(1); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
|
||||
if hdr.timeMS == 0xFFFFFF {
|
||||
if _, err = c.readSize(4); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
i0 = i1
|
||||
}
|
||||
|
||||
if _, err = io.ReadFull(c.rd, b[i0:]); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
|
||||
return hdr.tagType, timeMS, b, nil
|
||||
}
|
||||
func (c *Conn) writeMessage(chunkID, tagType byte, timeMS uint32, payload []byte) error {
|
||||
c.mu.Lock()
|
||||
c.resetBuffer()
|
||||
|
||||
b := payload
|
||||
size := uint32(len(b))
|
||||
|
||||
if size > c.wrPacketSize {
|
||||
c.appendType0(chunkID, tagType, timeMS, size, b[:c.wrPacketSize])
|
||||
|
||||
for {
|
||||
b = b[c.wrPacketSize:]
|
||||
if uint32(len(b)) > c.wrPacketSize {
|
||||
c.appendType3(chunkID, b[:c.wrPacketSize])
|
||||
} else {
|
||||
c.appendType3(chunkID, b)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
c.appendType0(chunkID, tagType, timeMS, size, b)
|
||||
}
|
||||
|
||||
//log.Printf("%d %2d %5d %6d %.32x", chunkID, tagType, timeMS, size, payload)
|
||||
|
||||
_, err := c.wr.Write(c.wrBuf)
|
||||
c.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Conn) resetBuffer() {
|
||||
c.wrBuf = c.wrBuf[:0]
|
||||
}
|
||||
|
||||
func (c *Conn) appendType0(chunkID, tagType byte, timeMS, size uint32, payload []byte) {
|
||||
// TODO: timeMS more than 24 bit
|
||||
c.wrBuf = append(c.wrBuf,
|
||||
chunkID,
|
||||
byte(timeMS>>16), byte(timeMS>>8), byte(timeMS),
|
||||
byte(size>>16), byte(size>>8), byte(size),
|
||||
tagType,
|
||||
c.streamID, 0, 0, 0, // little endian streamID
|
||||
)
|
||||
c.wrBuf = append(c.wrBuf, payload...)
|
||||
}
|
||||
|
||||
func (c *Conn) appendType3(chunkID byte, payload []byte) {
|
||||
c.wrBuf = append(c.wrBuf, 3<<6|chunkID)
|
||||
c.wrBuf = append(c.wrBuf, payload...)
|
||||
}
|
||||
|
||||
func (c *Conn) writePacketSize() error {
|
||||
b := binary.BigEndian.AppendUint32(nil, c.wrPacketSize)
|
||||
return c.writeMessage(2, TypeSetPacketSize, 0, b)
|
||||
}
|
||||
|
||||
func (c *Conn) writeConnect() error {
|
||||
b := amf.EncodeItems("connect", 1, map[string]any{
|
||||
"app": c.App,
|
||||
"flashVer": "FMLE/3.0 (compatible; FMSc/1.0)",
|
||||
"tcUrl": c.url,
|
||||
})
|
||||
if err := c.writeMessage(3, TypeCommand, 0, b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v, err := c.readResponse(1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
code := getString(v, 3, "code")
|
||||
if code != "NetConnection.Connect.Success" {
|
||||
return fmt.Errorf("rtmp: wrong response %#v", v)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) writeReleaseStream() error {
|
||||
b := amf.EncodeItems("releaseStream", 2, nil, c.Stream)
|
||||
if err := c.writeMessage(3, TypeCommand, 0, b); err != nil {
|
||||
return err
|
||||
}
|
||||
b = amf.EncodeItems("FCPublish", 3, nil, c.Stream)
|
||||
if err := c.writeMessage(3, TypeCommand, 0, b); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (c *Conn) writeCreateStream() error {
|
||||
b := amf.EncodeItems("createStream", 4, nil)
|
||||
if err := c.writeMessage(3, TypeCommand, 0, b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v, err := c.readResponse(4)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(v) == 4 {
|
||||
if f, ok := v[3].(float64); ok {
|
||||
c.streamID = byte(f)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("rtmp: wrong response %#v", v)
|
||||
}
|
||||
|
||||
func (c *Conn) writePublish() error {
|
||||
b := amf.EncodeItems("publish", 5, nil, c.Stream, "live")
|
||||
if err := c.writeMessage(3, TypeCommand, 0, b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v, err := c.readResponse(0)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
code := getString(v, 3, "code")
|
||||
if code != "NetStream.Publish.Start" {
|
||||
return fmt.Errorf("rtmp: wrong response %#v", v)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) writePlay() error {
|
||||
b := amf.EncodeItems("play", 5, nil, c.Stream)
|
||||
if err := c.writeMessage(3, TypeCommand, 0, b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v, err := c.readResponse(0)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
code := getString(v, 3, "code")
|
||||
if !strings.HasPrefix(code, "NetStream.Play.") {
|
||||
return fmt.Errorf("rtmp: wrong response %#v", v)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) readSize(n uint32) ([]byte, error) {
|
||||
b := make([]byte, n)
|
||||
if _, err := io.ReadAtLeast(c.rd, b, int(n)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func PutUint24(b []byte, v uint32) {
|
||||
_ = b[2]
|
||||
b[0] = byte(v >> 16)
|
||||
b[1] = byte(v >> 8)
|
||||
b[2] = byte(v)
|
||||
}
|
||||
|
||||
func Uint24(b []byte) uint32 {
|
||||
_ = b[2]
|
||||
return uint32(b[0])<<16 | uint32(b[1])<<8 | uint32(b[2])
|
||||
}
|
||||
|
||||
func getString(v []any, i int, key string) string {
|
||||
if len(v) <= i {
|
||||
return ""
|
||||
}
|
||||
if v, ok := v[i].(map[string]any); ok {
|
||||
if s, ok := v[key].(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
87
pkg/rtmp/flv.go
Normal file
87
pkg/rtmp/flv.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package rtmp
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/flv"
|
||||
)
|
||||
|
||||
func (c *Conn) Producer() (core.Producer, error) {
|
||||
c.rdBuf = []byte{
|
||||
'F', 'L', 'V', // signature
|
||||
1, // version
|
||||
0, // flags (has video/audio)
|
||||
0, 0, 0, 9, // header size
|
||||
}
|
||||
|
||||
return flv.Open(c)
|
||||
}
|
||||
|
||||
// Read - convert RTMP to FLV format
|
||||
func (c *Conn) Read(p []byte) (n int, err error) {
|
||||
// 1. Check temporary tempbuffer
|
||||
if len(c.rdBuf) == 0 {
|
||||
msgType, timeMS, payload, err2 := c.readMessage()
|
||||
if err2 != nil {
|
||||
return 0, err2
|
||||
}
|
||||
|
||||
// previous tag size (4 byte) + header (11 byte) + payload
|
||||
n = 4 + 11 + len(payload)
|
||||
|
||||
// 2. Check if the message fits in the buffer
|
||||
if n <= len(p) {
|
||||
encodeFLV(p, msgType, timeMS, payload)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Put the message into a temporary buffer
|
||||
c.rdBuf = make([]byte, n)
|
||||
encodeFLV(c.rdBuf, msgType, timeMS, payload)
|
||||
}
|
||||
|
||||
// 4. Send temporary buffer
|
||||
n = copy(p, c.rdBuf)
|
||||
c.rdBuf = c.rdBuf[n:]
|
||||
return
|
||||
}
|
||||
|
||||
func encodeFLV(b []byte, msgType byte, time uint32, payload []byte) {
|
||||
_ = b[4+11]
|
||||
|
||||
b[0] = 0
|
||||
b[1] = 0
|
||||
b[2] = 0
|
||||
b[3] = 0
|
||||
b[4+0] = msgType
|
||||
PutUint24(b[4+1:], uint32(len(payload)))
|
||||
PutUint24(b[4+4:], time)
|
||||
b[4+7] = byte(time >> 24)
|
||||
|
||||
copy(b[4+11:], payload)
|
||||
}
|
||||
|
||||
// Write - convert FLV format to RTMP format
|
||||
func (c *Conn) Write(p []byte) (n int, err error) {
|
||||
n = len(p)
|
||||
|
||||
if p[0] == 'F' {
|
||||
p = p[9+4:] // skip first msg with FLV header
|
||||
|
||||
for len(p) > 0 {
|
||||
size := 11 + uint16(p[2])<<8 + uint16(p[3]) + 4
|
||||
if _, err = c.Write(p[:size]); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
p = p[size:]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// decode FLV: 11 bytes header + payload + 4 byte size
|
||||
tagType := p[0]
|
||||
timeMS := uint32(p[4])<<16 | uint32(p[5])<<8 | uint32(p[6]) | uint32(p[7])<<24
|
||||
payload := p[11 : len(p)-4]
|
||||
|
||||
err = c.writeMessage(4, tagType, timeMS, payload)
|
||||
return
|
||||
}
|
@@ -1,28 +0,0 @@
|
||||
package rtmp
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/flv"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
)
|
||||
|
||||
func Dial(rawURL string) (core.Producer, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn, err := tcp.Dial(u, "1935", core.ConnDialTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rd, err := NewReader(u, conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return flv.Open(rd)
|
||||
}
|
@@ -1,488 +0,0 @@
|
||||
package rtmp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/flv/amf"
|
||||
)
|
||||
|
||||
const (
|
||||
MsgSetPacketSize = 1
|
||||
MsgServerBandwidth = 5
|
||||
MsgClientBandwidth = 6
|
||||
MsgCommand = 20
|
||||
|
||||
//MsgAck = 3
|
||||
//MsgControl = 4
|
||||
//MsgAudioPacket = 8
|
||||
//MsgVideoPacket = 9
|
||||
//MsgDataExt = 15
|
||||
//MsgCommandExt = 17
|
||||
//MsgData = 18
|
||||
)
|
||||
|
||||
var ErrResponse = errors.New("rtmp: wrong response")
|
||||
|
||||
type Reader struct {
|
||||
url string
|
||||
app string
|
||||
stream string
|
||||
pktSize uint32
|
||||
|
||||
headers map[uint32]*header
|
||||
|
||||
conn net.Conn
|
||||
rd io.Reader
|
||||
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func NewReader(u *url.URL, conn net.Conn) (*Reader, error) {
|
||||
rd := &Reader{
|
||||
url: u.String(),
|
||||
headers: map[uint32]*header{},
|
||||
conn: conn,
|
||||
rd: bufio.NewReaderSize(conn, core.BufferSize),
|
||||
}
|
||||
|
||||
if args := strings.Split(u.Path, "/"); len(args) >= 2 {
|
||||
rd.app = args[1]
|
||||
if len(args) >= 3 {
|
||||
rd.stream = args[2]
|
||||
if u.RawQuery != "" {
|
||||
rd.stream += "?" + u.RawQuery
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := rd.handshake(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rd.sendConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rd.sendConnect(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rd.sendPlay(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rd.buf = []byte{
|
||||
'F', 'L', 'V', // signature
|
||||
1, // version
|
||||
0, // flags (has video/audio)
|
||||
0, 0, 0, 9, // header size
|
||||
}
|
||||
|
||||
return rd, nil
|
||||
}
|
||||
|
||||
func (c *Reader) Read(p []byte) (n int, err error) {
|
||||
// 1. Check temporary tempbuffer
|
||||
if len(c.buf) == 0 {
|
||||
msgType, timeMS, payload, err2 := c.readMessage()
|
||||
if err2 != nil {
|
||||
return 0, err2
|
||||
}
|
||||
|
||||
payloadSize := len(payload)
|
||||
|
||||
// previous tag size (4 byte) + header (11 byte) + payload
|
||||
n = 4 + 11 + payloadSize
|
||||
|
||||
// 2. Check if the message fits in the buffer
|
||||
if n <= len(p) {
|
||||
encodeFLV(p, msgType, timeMS, uint32(payloadSize), payload)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Put the message into a temporary buffer
|
||||
c.buf = make([]byte, n)
|
||||
encodeFLV(c.buf, msgType, timeMS, uint32(payloadSize), payload)
|
||||
}
|
||||
|
||||
// 4. Send temporary buffer
|
||||
n = copy(p, c.buf)
|
||||
c.buf = c.buf[n:]
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Reader) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func encodeFLV(b []byte, msgType byte, time, size uint32, payload []byte) {
|
||||
b[0] = 0
|
||||
b[1] = 0
|
||||
b[2] = 0
|
||||
b[3] = 0
|
||||
b[4+0] = msgType
|
||||
PutUint24(b[4+1:], size)
|
||||
PutUint24(b[4+4:], time)
|
||||
b[4+7] = byte(time >> 24)
|
||||
|
||||
copy(b[4+11:], payload)
|
||||
}
|
||||
|
||||
type header struct {
|
||||
msgTime uint32
|
||||
msgSize uint32
|
||||
msgType byte
|
||||
}
|
||||
|
||||
func (c *Reader) readMessage() (byte, uint32, []byte, error) {
|
||||
hdrType, sid, err := c.readHeader()
|
||||
if err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
|
||||
// storing header information for support header type 3
|
||||
hdr, ok := c.headers[sid]
|
||||
if !ok {
|
||||
hdr = &header{}
|
||||
c.headers[sid] = hdr
|
||||
}
|
||||
|
||||
var b []byte
|
||||
|
||||
// https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol#Packet_structure
|
||||
switch hdrType {
|
||||
case 0: // 12 byte header (full header)
|
||||
if b, err = c.readSize(11); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
_ = b[7]
|
||||
hdr.msgTime = Uint24(b) // timestamp
|
||||
hdr.msgSize = Uint24(b[3:]) // msgdatalen
|
||||
hdr.msgType = b[6] // msgtypeid
|
||||
_ = binary.BigEndian.Uint32(b[7:]) // msgsid
|
||||
|
||||
case 1: // 8 bytes - like type b00, not including message ID (4 last bytes)
|
||||
if b, err = c.readSize(7); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
_ = b[6]
|
||||
hdr.msgTime = Uint24(b) // timestamp
|
||||
hdr.msgSize = Uint24(b[3:]) // msgdatalen
|
||||
hdr.msgType = b[6] // msgtypeid
|
||||
|
||||
case 2: // 4 bytes - Basic Header and timestamp (3 bytes) are included
|
||||
if b, err = c.readSize(3); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
hdr.msgTime = Uint24(b) // timestamp
|
||||
|
||||
case 3: // 1 byte - only the Basic Header is included
|
||||
// use here hdr from previous msg with same session ID (sid)
|
||||
}
|
||||
|
||||
timeMS := hdr.msgTime
|
||||
if timeMS == 0xFFFFFF {
|
||||
if b, err = c.readSize(4); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
timeMS = binary.BigEndian.Uint32(b)
|
||||
}
|
||||
|
||||
//log.Printf("[Reader] hdrType=%d sid=%d msdTime=%d msgSize=%d msgType=%d", hdrType, sid, hdr.msgTime, hdr.msgSize, hdr.msgType)
|
||||
|
||||
// 1. Response zero size
|
||||
if hdr.msgSize == 0 {
|
||||
return hdr.msgType, timeMS, nil, nil
|
||||
}
|
||||
|
||||
b = make([]byte, hdr.msgSize)
|
||||
|
||||
// 2. Response small packet
|
||||
if c.pktSize == 0 || hdr.msgSize < c.pktSize {
|
||||
if _, err = io.ReadFull(c.rd, b); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
return hdr.msgType, timeMS, b, nil
|
||||
}
|
||||
|
||||
// 3. Response big packet
|
||||
var i0 uint32
|
||||
for i1 := c.pktSize; i1 < hdr.msgSize; i1 += c.pktSize {
|
||||
if _, err = io.ReadFull(c.rd, b[i0:i1]); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
|
||||
if _, _, err = c.readHeader(); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
|
||||
if hdr.msgTime == 0xFFFFFF {
|
||||
if _, err = c.readSize(4); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
i0 = i1
|
||||
}
|
||||
|
||||
if _, err = io.ReadFull(c.rd, b[i0:]); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
|
||||
return hdr.msgType, timeMS, b, nil
|
||||
}
|
||||
|
||||
func (c *Reader) handshake() error {
|
||||
// simple handshake without real random and check response
|
||||
const randomSize = 4 + 4 + 1528
|
||||
|
||||
b := make([]byte, 1+randomSize)
|
||||
b[0] = 0x03
|
||||
if _, err := c.conn.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(c.rd, b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if b[0] != 3 {
|
||||
return errors.New("Reader: wrong handshake")
|
||||
}
|
||||
|
||||
if _, err := c.conn.Write(b[1:]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(c.rd, b[1:]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Reader) sendConfig() error {
|
||||
b := make([]byte, 5)
|
||||
binary.BigEndian.PutUint32(b, 65536)
|
||||
if err := c.sendRequest(MsgSetPacketSize, 0, b[:4]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint32(b, 2500000)
|
||||
if err := c.sendRequest(MsgServerBandwidth, 0, b[:4]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint32(b, 10000000) // ack size
|
||||
b[4] = 2 // limit type
|
||||
if err := c.sendRequest(MsgClientBandwidth, 0, b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Reader) sendConnect() error {
|
||||
msg := amf.AMF{}
|
||||
msg.WriteString("connect")
|
||||
msg.WriteNumber(1)
|
||||
msg.WriteObject(map[string]any{
|
||||
"app": c.app,
|
||||
"flashVer": "MAC 32,0,0,0",
|
||||
"tcUrl": c.url,
|
||||
"fpad": false, // ?
|
||||
"capabilities": 15, // ?
|
||||
"audioCodecs": 4071, // ?
|
||||
"videoCodecs": 252, // ?
|
||||
"videoFunction": 1, // ?
|
||||
})
|
||||
|
||||
if err := c.sendRequest(MsgCommand, 0, msg.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s, err := c.waitCode("_result", float64(1)) // result with same ID
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s != "NetConnection.Connect.Success" {
|
||||
return errors.New("Reader: wrong code: " + s)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Reader) sendPlay() error {
|
||||
msg := amf.NewWriter()
|
||||
msg.WriteString("createStream")
|
||||
msg.WriteNumber(2)
|
||||
msg.WriteNull()
|
||||
|
||||
if err := c.sendRequest(MsgCommand, 0, msg.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args, err := c.waitResponse("_result", float64(2)) // result with same ID
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(args) < 4 {
|
||||
return ErrResponse
|
||||
}
|
||||
|
||||
sid, _ := args[3].(float64)
|
||||
|
||||
msg = amf.NewWriter()
|
||||
msg.WriteString("play")
|
||||
msg.WriteNumber(0)
|
||||
msg.WriteNull()
|
||||
msg.WriteString(c.stream)
|
||||
|
||||
if err = c.sendRequest(MsgCommand, uint32(sid), msg.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s, err := c.waitCode("onStatus", float64(0)) // events has zero transaction ID
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch s {
|
||||
case "NetStream.Play.Start", "NetStream.Play.Reset":
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("Reader: wrong code: " + s)
|
||||
}
|
||||
|
||||
func (c *Reader) sendRequest(msgType byte, streamID uint32, payload []byte) error {
|
||||
n := len(payload)
|
||||
b := make([]byte, 12+n)
|
||||
_ = b[12]
|
||||
|
||||
switch msgType {
|
||||
case MsgSetPacketSize, MsgServerBandwidth, MsgClientBandwidth:
|
||||
b[0] = 0x02 // chunk ID
|
||||
case MsgCommand:
|
||||
if streamID == 0 {
|
||||
b[0] = 0x03 // chunk ID
|
||||
} else {
|
||||
b[0] = 0x08 // chunk ID
|
||||
}
|
||||
}
|
||||
|
||||
//PutUint24(b[1:], 0) // timestamp
|
||||
PutUint24(b[4:], uint32(n)) // payload size
|
||||
b[7] = msgType // message type
|
||||
binary.BigEndian.PutUint32(b[8:], streamID) // message stream ID
|
||||
copy(b[12:], payload)
|
||||
|
||||
if _, err := c.conn.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Reader) readHeader() (byte, uint32, error) {
|
||||
b, err := c.readSize(1)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
hdrType := b[0] >> 6
|
||||
sid := uint32(b[0] & 0b111111)
|
||||
|
||||
switch sid {
|
||||
case 0:
|
||||
if b, err = c.readSize(1); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
sid = 64 + uint32(b[0])
|
||||
case 1:
|
||||
if b, err = c.readSize(2); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
sid = 64 + uint32(binary.BigEndian.Uint16(b))
|
||||
}
|
||||
|
||||
return hdrType, sid, nil
|
||||
}
|
||||
|
||||
func (c *Reader) readSize(n uint32) ([]byte, error) {
|
||||
b := make([]byte, n)
|
||||
if _, err := io.ReadAtLeast(c.rd, b, int(n)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (c *Reader) waitResponse(cmd any, tid any) ([]any, error) {
|
||||
for {
|
||||
msgType, _, b, err := c.readMessage()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch msgType {
|
||||
case MsgSetPacketSize:
|
||||
c.pktSize = binary.BigEndian.Uint32(b)
|
||||
|
||||
case MsgCommand:
|
||||
var v []any
|
||||
if v, err = amf.NewReader(b).ReadItems(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(v) < 4 {
|
||||
return nil, ErrResponse
|
||||
}
|
||||
|
||||
if v[0] == cmd && v[1] == tid {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Reader) waitCode(cmd any, tid any) (string, error) {
|
||||
args, err := c.waitResponse(cmd, tid)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(args) < 4 {
|
||||
return "", ErrResponse
|
||||
}
|
||||
|
||||
m, _ := args[3].(map[string]any)
|
||||
if m == nil {
|
||||
return "", ErrResponse
|
||||
}
|
||||
|
||||
v, _ := m["code"]
|
||||
if v == nil {
|
||||
return "", ErrResponse
|
||||
}
|
||||
|
||||
s, _ := v.(string)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func PutUint24(b []byte, v uint32) {
|
||||
_ = b[2]
|
||||
b[0] = byte(v >> 16)
|
||||
b[1] = byte(v >> 8)
|
||||
b[2] = byte(v)
|
||||
}
|
||||
|
||||
func Uint24(b []byte) uint32 {
|
||||
_ = b[2]
|
||||
return uint32(b[0])<<16 | uint32(b[1])<<8 | uint32(b[2])
|
||||
}
|
162
pkg/rtmp/server.go
Normal file
162
pkg/rtmp/server.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package rtmp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/flv/amf"
|
||||
)
|
||||
|
||||
func NewServer(conn net.Conn) (*Conn, error) {
|
||||
c := &Conn{
|
||||
conn: conn,
|
||||
rd: bufio.NewReaderSize(conn, core.BufferSize),
|
||||
wr: conn,
|
||||
|
||||
chunks: map[uint8]*header{},
|
||||
|
||||
rdPacketSize: 128,
|
||||
wrPacketSize: 4096,
|
||||
}
|
||||
|
||||
if err := c.serverHandshake(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.writePacketSize(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Conn) serverHandshake() error {
|
||||
b := make([]byte, 1+1536)
|
||||
// read C0+C1
|
||||
if _, err := io.ReadFull(c.rd, b); err != nil {
|
||||
return err
|
||||
}
|
||||
// write S0+S1, skip random
|
||||
if _, err := c.conn.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
// read S1, skip check
|
||||
if _, err := io.ReadFull(c.rd, make([]byte, 1536)); err != nil {
|
||||
return err
|
||||
}
|
||||
// write C1
|
||||
if _, err := c.conn.Write(b[1:]); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) ReadCommands() error {
|
||||
for {
|
||||
msgType, _, b, err := c.readMessage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//log.Printf("%d %.256x", msgType, b)
|
||||
|
||||
switch msgType {
|
||||
case TypeSetPacketSize:
|
||||
c.rdPacketSize = binary.BigEndian.Uint32(b)
|
||||
case TypeCommand:
|
||||
if err = c.acceptCommand(b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Intent != "" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
CommandConnect = "connect"
|
||||
CommandReleaseStream = "releaseStream"
|
||||
CommandFCPublish = "FCPublish"
|
||||
CommandCreateStream = "createStream"
|
||||
CommandPublish = "publish"
|
||||
CommandPlay = "play"
|
||||
)
|
||||
|
||||
func (c *Conn) acceptCommand(b []byte) error {
|
||||
items, err := amf.NewReader(b).ReadItems()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
//log.Printf("%#v", items)
|
||||
|
||||
if len(items) < 2 {
|
||||
return fmt.Errorf("rtmp: read command %x", b)
|
||||
}
|
||||
|
||||
cmd, ok := items[0].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("rtmp: read command %x", b)
|
||||
}
|
||||
|
||||
tID, ok := items[1].(float64) // transaction ID
|
||||
if !ok {
|
||||
return fmt.Errorf("rtmp: read command %x", b)
|
||||
}
|
||||
|
||||
switch cmd {
|
||||
case CommandConnect:
|
||||
if len(items) == 3 {
|
||||
if v, ok := items[2].(map[string]any); ok {
|
||||
c.App, _ = v["app"].(string)
|
||||
}
|
||||
}
|
||||
|
||||
if c.App == "" {
|
||||
return fmt.Errorf("rtmp: read command %x", b)
|
||||
}
|
||||
|
||||
payload := amf.EncodeItems(
|
||||
"_result", tID,
|
||||
map[string]any{"fmsVer": "FMS/3,0,1,123"},
|
||||
map[string]any{"code": "NetConnection.Connect.Success"},
|
||||
)
|
||||
return c.writeMessage(3, TypeCommand, 0, payload)
|
||||
|
||||
case CommandReleaseStream:
|
||||
payload := amf.EncodeItems("_result", tID, nil)
|
||||
return c.writeMessage(3, TypeCommand, 0, payload)
|
||||
|
||||
case CommandCreateStream:
|
||||
payload := amf.EncodeItems("_result", tID, nil, 1)
|
||||
return c.writeMessage(3, TypeCommand, 0, payload)
|
||||
|
||||
case CommandPublish, CommandPlay: // response later
|
||||
c.Intent = cmd
|
||||
c.streamID = 1
|
||||
|
||||
case CommandFCPublish: // no response
|
||||
|
||||
default:
|
||||
println("rtmp: unknown command: " + cmd)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) WriteStart() error {
|
||||
var code string
|
||||
if c.Intent == CommandPublish {
|
||||
code = "NetStream.Publish.Start"
|
||||
} else {
|
||||
code = "NetStream.Play.Start"
|
||||
}
|
||||
|
||||
payload := amf.EncodeItems("onStatus", 0, nil, map[string]any{"code": code})
|
||||
return c.writeMessage(3, TypeCommand, 0, payload)
|
||||
}
|
@@ -35,7 +35,7 @@ func (c *Conn) Dial() (err error) {
|
||||
if c.Timeout != 0 {
|
||||
timeout = time.Second * time.Duration(c.Timeout)
|
||||
}
|
||||
conn, err = tcp.Dial(c.URL, "554", timeout)
|
||||
conn, err = tcp.Dial(c.URL, timeout)
|
||||
} else {
|
||||
conn, err = websocket.Dial(c.Transport)
|
||||
}
|
||||
|
@@ -111,12 +111,12 @@ func (c *Conn) Handle() (err error) {
|
||||
|
||||
if c.Timeout == 0 {
|
||||
// polling frames from remote RTSP Server (ex Camera)
|
||||
if len(c.receivers) > 0 {
|
||||
// if we receiving video/audio from camera
|
||||
timeout = time.Second * 5
|
||||
} else {
|
||||
timeout = time.Second * 5
|
||||
|
||||
if len(c.receivers) == 0 {
|
||||
// if we only send audio to camera
|
||||
timeout = time.Second * 30
|
||||
// https://github.com/AlexxIT/go2rtc/issues/659
|
||||
timeout += keepaliveDT
|
||||
}
|
||||
} else {
|
||||
timeout = time.Second * time.Duration(c.Timeout)
|
||||
|
@@ -2,7 +2,9 @@ package shell
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"syscall"
|
||||
@@ -75,3 +77,20 @@ func RunUntilSignal() {
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
println("exit with signal:", (<-sigs).String())
|
||||
}
|
||||
|
||||
// Restart idea taken from https://github.com/tillberg/autorestart
|
||||
// Copyright (c) 2015, Dan Tillberg
|
||||
func Restart() {
|
||||
path, err := exec.LookPath(os.Args[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
path, err = filepath.Abs(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
path = filepath.Clean(path)
|
||||
if err = syscall.Exec(path, os.Args, os.Environ()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@ import (
|
||||
func (c *Client) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
|
||||
if c.sender == nil {
|
||||
if err := c.SetupBackchannel(); err != nil {
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
muxer := mpegts.NewMuxer()
|
||||
|
@@ -10,13 +10,22 @@ import (
|
||||
)
|
||||
|
||||
// Dial - for RTSP(S|X) and RTMP(S|X)
|
||||
func Dial(u *url.URL, port string, timeout time.Duration) (net.Conn, error) {
|
||||
func Dial(u *url.URL, timeout time.Duration) (net.Conn, error) {
|
||||
var address string
|
||||
var hostname string // without port
|
||||
if i := strings.IndexByte(u.Host, ':'); i > 0 {
|
||||
address = u.Host
|
||||
hostname = u.Host[:i]
|
||||
} else {
|
||||
switch u.Scheme {
|
||||
case "rtsp", "rtsps", "rtspx":
|
||||
address = u.Host + ":554"
|
||||
case "rtmp":
|
||||
address = u.Host + ":1935"
|
||||
case "rtmps", "rtmpx":
|
||||
address = u.Host + ":443"
|
||||
}
|
||||
hostname = u.Host
|
||||
u.Host += ":" + port
|
||||
}
|
||||
|
||||
var secure *tls.Config
|
||||
@@ -33,7 +42,7 @@ func Dial(u *url.URL, port string, timeout time.Duration) (net.Conn, error) {
|
||||
return nil, errors.New("unsupported scheme: " + u.Scheme)
|
||||
}
|
||||
|
||||
conn, err := net.DialTimeout("tcp", u.Host, timeout)
|
||||
conn, err := net.DialTimeout("tcp", address, timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<title>File Editor</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">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.24.1/ace.min.js"></script>
|
||||
<script src="https://unpkg.com/ace-builds@1.28.0/src-min/ace.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
@@ -31,7 +31,7 @@
|
||||
<script>
|
||||
let dump;
|
||||
|
||||
ace.config.set('basePath', 'https://cdnjs.cloudflare.com/ajax/libs/ace/1.24.1/');
|
||||
ace.config.set('basePath', 'https://unpkg.com/ace-builds@1.28.0/src-min/');
|
||||
const editor = ace.edit('config', {
|
||||
mode: 'ace/mode/yaml',
|
||||
});
|
||||
@@ -46,7 +46,7 @@
|
||||
r = await fetch('api/config', {method: 'POST', body: editor.getValue()});
|
||||
if (r.ok) {
|
||||
alert('OK');
|
||||
fetch('api/exit?code=100', {method: 'POST'});
|
||||
fetch('api/restart', {method: 'POST'});
|
||||
} else {
|
||||
alert(await r.text());
|
||||
}
|
||||
|
@@ -90,15 +90,32 @@
|
||||
<div>
|
||||
<h2>Play audio</h2>
|
||||
<pre>example: ffmpeg:https://example.com/song.mp3#audio=pcma#input=file</pre>
|
||||
<input id="source" type="text" placeholder="source">
|
||||
<a id="send" href="#">send</a> / cameras with two way audio support
|
||||
<input id="play-url" type="text" placeholder="url">
|
||||
<a id="play-send" href="#">send</a> / cameras with two way audio support
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('send').addEventListener('click', ev => {
|
||||
document.getElementById('play-send').addEventListener('click', ev => {
|
||||
ev.preventDefault();
|
||||
const url = new URL('api/streams', location.href);
|
||||
url.searchParams.set('dst', src);
|
||||
url.searchParams.set('src', document.getElementById('source').value);
|
||||
url.searchParams.set('src', document.getElementById('play-url').value);
|
||||
fetch(url, {method: 'POST'});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<h2>Publish stream</h2>
|
||||
<pre>YouTube: rtmps://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx
|
||||
Telegram: rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx</pre>
|
||||
<input id="pub-url" type="text" placeholder="url">
|
||||
<a id="pub-send" href="#">send</a> / Telegram RTMPS server
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('pub-send').addEventListener('click', ev => {
|
||||
ev.preventDefault();
|
||||
const url = new URL('api/streams', location.href);
|
||||
url.searchParams.set('src', src);
|
||||
url.searchParams.set('dst', document.getElementById('pub-url').value);
|
||||
fetch(url, {method: 'POST'});
|
||||
});
|
||||
</script>
|
||||
|
141
www/video-rtc.js
141
www/video-rtc.js
@@ -7,6 +7,7 @@
|
||||
* - ECMAScript 2017 (ES8) = ES6 + async
|
||||
* - RTCPeerConnection for Safari iOS 11.0+
|
||||
* - IntersectionObserver for Safari iOS 12.2+
|
||||
* - ManagedMediaSource for Safari 17+
|
||||
*
|
||||
* Doesn't support:
|
||||
* - MediaSource for Safari iOS
|
||||
@@ -37,6 +38,12 @@ export class VideoRTC extends HTMLElement {
|
||||
*/
|
||||
this.mode = 'webrtc,mse,hls,mjpeg';
|
||||
|
||||
/**
|
||||
* [Config] Requested medias (video, audio, microphone).
|
||||
* @type {string}
|
||||
*/
|
||||
this.media = 'video,audio';
|
||||
|
||||
/**
|
||||
* [config] Run stream when not displayed on the screen. Default `false`.
|
||||
* @type {boolean}
|
||||
@@ -128,8 +135,8 @@ export class VideoRTC extends HTMLElement {
|
||||
this.ondata = null;
|
||||
|
||||
/**
|
||||
* [internal] Handlers list for receiving JSON from WebSocket
|
||||
* @type {Object.<string,Function>}}
|
||||
* [internal] Handlers list for receiving JSON from WebSocket.
|
||||
* @type {Object.<string,Function>}
|
||||
*/
|
||||
this.onmessage = null;
|
||||
}
|
||||
@@ -174,11 +181,11 @@ export class VideoRTC extends HTMLElement {
|
||||
if (this.ws) this.ws.send(JSON.stringify(value));
|
||||
}
|
||||
|
||||
codecs(type) {
|
||||
const test = type === 'mse'
|
||||
? codec => MediaSource.isTypeSupported(`video/mp4; codecs="${codec}"`)
|
||||
: codec => this.video.canPlayType(`video/mp4; codecs="${codec}"`);
|
||||
return this.CODECS.filter(test).join();
|
||||
/** @param {Function} isSupported */
|
||||
codecs(isSupported) {
|
||||
return this.CODECS
|
||||
.filter(codec => this.media.indexOf(codec.indexOf('vc1') > 0 ? 'video' : 'audio') >= 0)
|
||||
.filter(codec => isSupported(`video/mp4; codecs="${codec}"`)).join();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -303,6 +310,9 @@ export class VideoRTC extends HTMLElement {
|
||||
|
||||
this.pcState = WebSocket.CLOSED;
|
||||
if (this.pc) {
|
||||
this.pc.getSenders().forEach(sender => {
|
||||
if (sender.track) sender.track.stop();
|
||||
});
|
||||
this.pc.close();
|
||||
this.pc = null;
|
||||
}
|
||||
@@ -334,7 +344,7 @@ export class VideoRTC extends HTMLElement {
|
||||
|
||||
const modes = [];
|
||||
|
||||
if (this.mode.indexOf('mse') >= 0 && 'MediaSource' in window) { // iPhone
|
||||
if (this.mode.indexOf('mse') >= 0 && ('MediaSource' in window || 'ManagedMediaSource' in window)) {
|
||||
modes.push('mse');
|
||||
this.onmse();
|
||||
} else if (this.mode.indexOf('hls') >= 0 && this.video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
@@ -345,7 +355,7 @@ export class VideoRTC extends HTMLElement {
|
||||
this.onmp4();
|
||||
}
|
||||
|
||||
if (this.mode.indexOf('webrtc') >= 0 && 'RTCPeerConnection' in window) { // macOS Desktop app
|
||||
if (this.mode.indexOf('webrtc') >= 0 && 'RTCPeerConnection' in window) {
|
||||
modes.push('webrtc');
|
||||
this.onwebrtc();
|
||||
}
|
||||
@@ -387,14 +397,30 @@ export class VideoRTC extends HTMLElement {
|
||||
}
|
||||
|
||||
onmse() {
|
||||
const ms = new MediaSource();
|
||||
ms.addEventListener('sourceopen', () => {
|
||||
URL.revokeObjectURL(this.video.src);
|
||||
this.send({type: 'mse', value: this.codecs('mse')});
|
||||
}, {once: true});
|
||||
/** @type {MediaSource} */
|
||||
let ms;
|
||||
|
||||
if ('ManagedMediaSource' in window) {
|
||||
const MediaSource = window.ManagedMediaSource;
|
||||
|
||||
ms = new MediaSource();
|
||||
ms.addEventListener('sourceopen', () => {
|
||||
this.send({type: 'mse', value: this.codecs(MediaSource.isTypeSupported)});
|
||||
}, {once: true});
|
||||
|
||||
this.video.disableRemotePlayback = true;
|
||||
this.video.srcObject = ms;
|
||||
} else {
|
||||
ms = new MediaSource();
|
||||
ms.addEventListener('sourceopen', () => {
|
||||
URL.revokeObjectURL(this.video.src);
|
||||
this.send({type: 'mse', value: this.codecs(MediaSource.isTypeSupported)});
|
||||
}, {once: true});
|
||||
|
||||
this.video.src = URL.createObjectURL(ms);
|
||||
this.video.srcObject = null;
|
||||
}
|
||||
|
||||
this.video.src = URL.createObjectURL(ms);
|
||||
this.video.srcObject = null;
|
||||
this.play();
|
||||
|
||||
this.mseCodecs = '';
|
||||
@@ -451,10 +477,6 @@ export class VideoRTC extends HTMLElement {
|
||||
onwebrtc() {
|
||||
const pc = new RTCPeerConnection(this.pcConfig);
|
||||
|
||||
/** @type {HTMLVideoElement} */
|
||||
const video2 = document.createElement('video');
|
||||
video2.addEventListener('loadeddata', ev => this.onpcvideo(ev), {once: true});
|
||||
|
||||
pc.addEventListener('icecandidate', ev => {
|
||||
if (ev.candidate && this.mode.indexOf('webrtc/tcp') >= 0 && ev.candidate.protocol === 'udp') return;
|
||||
|
||||
@@ -462,21 +484,14 @@ export class VideoRTC extends HTMLElement {
|
||||
this.send({type: 'webrtc/candidate', value: candidate});
|
||||
});
|
||||
|
||||
pc.addEventListener('track', ev => {
|
||||
// when stream already init
|
||||
if (video2.srcObject !== null) return;
|
||||
|
||||
// when audio track not exist in Chrome
|
||||
if (ev.streams.length === 0) return;
|
||||
|
||||
// when audio track not exist in Firefox
|
||||
if (ev.streams[0].id[0] === '{') return;
|
||||
|
||||
video2.srcObject = ev.streams[0];
|
||||
});
|
||||
|
||||
pc.addEventListener('connectionstatechange', () => {
|
||||
if (pc.connectionState === 'failed' || pc.connectionState === 'disconnected') {
|
||||
if (pc.connectionState === 'connected') {
|
||||
const tracks = pc.getReceivers().map(receiver => receiver.track);
|
||||
/** @type {HTMLVideoElement} */
|
||||
const video2 = document.createElement('video');
|
||||
video2.addEventListener('loadeddata', () => this.onpcvideo(video2), {once: true});
|
||||
video2.srcObject = new MediaStream(tracks);
|
||||
} else if (pc.connectionState === 'failed' || pc.connectionState === 'disconnected') {
|
||||
pc.close(); // stop next events
|
||||
|
||||
this.pcState = WebSocket.CLOSED;
|
||||
@@ -506,14 +521,8 @@ export class VideoRTC extends HTMLElement {
|
||||
}
|
||||
};
|
||||
|
||||
// Safari doesn't support "offerToReceiveVideo"
|
||||
pc.addTransceiver('video', {direction: 'recvonly'});
|
||||
pc.addTransceiver('audio', {direction: 'recvonly'});
|
||||
|
||||
pc.createOffer().then(offer => {
|
||||
pc.setLocalDescription(offer).then(() => {
|
||||
this.send({type: 'webrtc/offer', value: offer.sdp});
|
||||
});
|
||||
this.createOffer(pc).then(offer => {
|
||||
this.send({type: 'webrtc/offer', value: offer.sdp});
|
||||
});
|
||||
|
||||
this.pcState = WebSocket.CONNECTING;
|
||||
@@ -521,31 +530,51 @@ export class VideoRTC extends HTMLElement {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ev {Event}
|
||||
* @param pc {RTCPeerConnection}
|
||||
* @return {Promise<RTCSessionDescriptionInit>}
|
||||
*/
|
||||
onpcvideo(ev) {
|
||||
if (!this.pc) return;
|
||||
async createOffer(pc) {
|
||||
try {
|
||||
if (this.media.indexOf('microphone') >= 0) {
|
||||
const media = await navigator.mediaDevices.getUserMedia({audio: true});
|
||||
media.getTracks().forEach(track => {
|
||||
pc.addTransceiver(track, {direction: 'sendonly'});
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
|
||||
/** @type {HTMLVideoElement} */
|
||||
const video2 = ev.target;
|
||||
const state = this.pc.connectionState;
|
||||
for (const kind of ['video', 'audio']) {
|
||||
if (this.media.indexOf(kind) >= 0) {
|
||||
pc.addTransceiver(kind, {direction: 'recvonly'});
|
||||
}
|
||||
}
|
||||
|
||||
// Firefox doesn't support pc.connectionState
|
||||
if (state === 'connected' || state === 'connecting' || !state) {
|
||||
const offer = await pc.createOffer();
|
||||
await pc.setLocalDescription(offer);
|
||||
return offer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param video2 {HTMLVideoElement}
|
||||
*/
|
||||
onpcvideo(video2) {
|
||||
if (this.pc) {
|
||||
// Video+Audio > Video, H265 > H264, Video > Audio, WebRTC > MSE
|
||||
let rtcPriority = 0, msePriority = 0;
|
||||
|
||||
/** @type {MediaStream} */
|
||||
const ms = video2.srcObject;
|
||||
if (ms.getVideoTracks().length > 0) rtcPriority += 0x220;
|
||||
if (ms.getAudioTracks().length > 0) rtcPriority += 0x102;
|
||||
const stream = video2.srcObject;
|
||||
if (stream.getVideoTracks().length > 0) rtcPriority += 0x220;
|
||||
if (stream.getAudioTracks().length > 0) rtcPriority += 0x102;
|
||||
|
||||
if (this.mseCodecs.indexOf('hvc1.') >= 0) msePriority += 0x230;
|
||||
if (this.mseCodecs.indexOf('avc1.') >= 0) msePriority += 0x210;
|
||||
if (this.mseCodecs.indexOf('mp4a.') >= 0) msePriority += 0x101;
|
||||
|
||||
if (rtcPriority >= msePriority) {
|
||||
this.video.srcObject = ms;
|
||||
this.video.srcObject = stream;
|
||||
this.play();
|
||||
|
||||
this.pcState = WebSocket.OPEN;
|
||||
@@ -586,7 +615,7 @@ export class VideoRTC extends HTMLElement {
|
||||
this.play();
|
||||
};
|
||||
|
||||
this.send({type: 'hls', value: this.codecs('hls')});
|
||||
this.send({type: 'hls', value: this.codecs(type => this.video.canPlayType(type))});
|
||||
}
|
||||
|
||||
onmp4() {
|
||||
@@ -618,7 +647,7 @@ export class VideoRTC extends HTMLElement {
|
||||
video2.src = 'data:video/mp4;base64,' + VideoRTC.btoa(data);
|
||||
};
|
||||
|
||||
this.send({type: 'mp4', value: this.codecs('mp4')});
|
||||
this.send({type: 'mp4', value: this.codecs(this.video.canPlayType)});
|
||||
}
|
||||
|
||||
static btoa(buffer) {
|
||||
|
Reference in New Issue
Block a user