Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 20d45bff92 | |||
| 4ad67e9f6f | |||
| e367940bd9 | |||
| 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 | |||
| c8ac6b2271 | |||
| 28f5c2b974 | |||
| daa2522a52 | |||
| 863f8ec19b | |||
| 8f98fc4547 | |||
| 398afbe49f | |||
| ad8c0ab2fb | |||
| 37130576e9 | |||
| 486fea2227 | |||
| 6d7357b151 | |||
| 452d7577f8 | |||
| 124398115e | |||
| 541a7b28a7 | |||
| 947b0970ad | |||
| 447fd5b3eb | |||
| 064ffef462 | |||
| 05360ac284 | |||
| 08dabc7331 | |||
| d724df7db2 |
@@ -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.
|
||||
@@ -837,7 +890,7 @@ HomeKit module can work in two modes:
|
||||
streams:
|
||||
dahua1: rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0
|
||||
homekit:
|
||||
dahua1: # same stream ID from streams list, default PIN - 195502224
|
||||
dahua1: # same stream ID from streams list, default PIN - 19550224
|
||||
```
|
||||
|
||||
**Full config**
|
||||
@@ -851,7 +904,7 @@ streams:
|
||||
|
||||
homekit:
|
||||
dahua1: # same stream ID from streams list
|
||||
pin: 12345678 # custom PIN, default: 195502224
|
||||
pin: 12345678 # custom PIN, default: 19550224
|
||||
name: Dahua camera # custom camera name, default: generated from stream ID
|
||||
device_id: dahua1 # custom ID, default: generated from stream ID
|
||||
device_private: dahua1 # custom key, default: generated from stream ID
|
||||
@@ -965,7 +1018,7 @@ You have several options on how to add a camera to Home Assistant:
|
||||
- Install any [go2rtc](#fast-start)
|
||||
- Add your stream to [go2rtc config](#configuration)
|
||||
- Hass > Settings > Integrations > Add Integration > [ONVIF](https://my.home-assistant.io/redirect/config_flow_start/?domain=onvif) > Host: `127.0.0.1`, Port: `1984`
|
||||
- Hass > Settings > Integrations > Add Integration > [Generic Camera](https://my.home-assistant.io/redirect/config_flow_start/?domain=generic) > `rtsp://127.0.0.1:8554/camera1` (change to your stream name)
|
||||
- Hass > Settings > Integrations > Add Integration > [Generic Camera](https://my.home-assistant.io/redirect/config_flow_start/?domain=generic) > Stream Source URL: `rtsp://127.0.0.1:8554/camera1` (change to your stream name, leave everything else as is)
|
||||
|
||||
You have several options on how to watch the stream from the cameras in Home Assistant:
|
||||
|
||||
|
||||
+67
-23
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -4,21 +4,21 @@ go 1.21
|
||||
|
||||
require (
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/miekg/dns v1.1.55
|
||||
github.com/pion/ice/v2 v2.3.10
|
||||
github.com/pion/interceptor v0.1.17
|
||||
github.com/miekg/dns v1.1.56
|
||||
github.com/pion/ice/v2 v2.3.11
|
||||
github.com/pion/interceptor v0.1.22
|
||||
github.com/pion/rtcp v1.2.10
|
||||
github.com/pion/rtp v1.8.1
|
||||
github.com/pion/rtp v1.8.2
|
||||
github.com/pion/sdp/v3 v3.0.6
|
||||
github.com/pion/srtp/v2 v2.0.16
|
||||
github.com/pion/srtp/v2 v2.0.17
|
||||
github.com/pion/stun v0.6.1
|
||||
github.com/pion/webrtc/v3 v3.2.17
|
||||
github.com/rs/zerolog v1.30.0
|
||||
github.com/pion/webrtc/v3 v3.2.21
|
||||
github.com/rs/zerolog v1.31.0
|
||||
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3
|
||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
|
||||
golang.org/x/crypto v0.12.0
|
||||
golang.org/x/crypto v0.14.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -31,14 +31,14 @@ require (
|
||||
github.com/pion/datachannel v1.5.5 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.7 // indirect
|
||||
github.com/pion/logging v0.2.2 // indirect
|
||||
github.com/pion/mdns v0.0.7 // indirect
|
||||
github.com/pion/mdns v0.0.9 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/sctp v1.8.8 // indirect
|
||||
github.com/pion/transport/v2 v2.2.1 // indirect
|
||||
github.com/pion/turn/v2 v2.1.3 // indirect
|
||||
github.com/pion/sctp v1.8.9 // indirect
|
||||
github.com/pion/transport/v2 v2.2.4 // indirect
|
||||
github.com/pion/turn/v2 v2.1.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/net v0.14.0 // indirect
|
||||
golang.org/x/sys v0.11.0 // indirect
|
||||
golang.org/x/tools v0.12.0 // indirect
|
||||
golang.org/x/mod v0.13.0 // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/tools v0.14.0 // indirect
|
||||
)
|
||||
|
||||
@@ -19,7 +19,6 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
@@ -40,6 +39,8 @@ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APP
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
|
||||
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
|
||||
github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE=
|
||||
github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
@@ -53,47 +54,61 @@ github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew
|
||||
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
|
||||
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
|
||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/ice/v2 v2.3.10 h1:T3bUJKqh7pGEdMyTngUcTeQd6io9X8JjgsVWZDannnY=
|
||||
github.com/pion/ice/v2 v2.3.10/go.mod h1:hHGCibDfmXGqukayQw979xEctASp2Pe5Oe0iDU8pRus=
|
||||
github.com/pion/interceptor v0.1.17 h1:prJtgwFh/gB8zMqGZoOgJPHivOwVAp61i2aG61Du/1w=
|
||||
github.com/pion/interceptor v0.1.17/go.mod h1:SY8kpmfVBvrbUzvj2bsXz7OJt5JvmVNZ+4Kjq7FcwrI=
|
||||
github.com/pion/ice/v2 v2.3.11 h1:rZjVmUwyT55cmN8ySMpL7rsS8KYsJERsrxJLLxpKhdw=
|
||||
github.com/pion/ice/v2 v2.3.11/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E=
|
||||
github.com/pion/interceptor v0.1.18/go.mod h1:tpvvF4cPM6NGxFA1DUMbhabzQBxdWMATDGEUYOR9x6I=
|
||||
github.com/pion/interceptor v0.1.19 h1:tq0TGBzuZQqipyBhaC1mVUCfCh8XjDKUuibq9rIl5t4=
|
||||
github.com/pion/interceptor v0.1.19/go.mod h1:VANhFxdJezB8mwToMMmrmyHyP9gym6xLqIUch31xryg=
|
||||
github.com/pion/interceptor v0.1.22 h1:khhimAF0/VmGaIfeE+bA3X1jm0lD8C8HOGcU7vpWcPA=
|
||||
github.com/pion/interceptor v0.1.22/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/mdns v0.0.7 h1:P0UB4Sr6xDWEox0kTVxF0LmQihtCbSAdW0H2nEgkA3U=
|
||||
github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8=
|
||||
github.com/pion/mdns v0.0.8/go.mod h1:hYE72WX8WDveIhg7fmXgMKivD3Puklk0Ymzog0lSyaI=
|
||||
github.com/pion/mdns v0.0.9 h1:7Ue5KZsqq8EuqStnpPWV33vYYEH0+skdDN5L7EiEsI4=
|
||||
github.com/pion/mdns v0.0.9/go.mod h1:2JA5exfxwzXiCihmxpTKgFUpiQws2MnipoPK09vecIc=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc=
|
||||
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
|
||||
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
||||
github.com/pion/rtp v1.8.0/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/rtp v1.8.1 h1:26OxTc6lKg/qLSGir5agLyj0QKaOv8OP5wps2SFnVNQ=
|
||||
github.com/pion/rtp v1.8.1/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/rtp v1.8.2 h1:oKMM0K1/QYQ5b5qH+ikqDSZRipP5mIxPJcgcvw5sH0w=
|
||||
github.com/pion/rtp v1.8.2/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
|
||||
github.com/pion/sctp v1.8.8 h1:5EdnnKI4gpyR1a1TwbiS/wxEgcUWBHsc7ILAjARJB+U=
|
||||
github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs=
|
||||
github.com/pion/sctp v1.8.9 h1:TP5ZVxV5J7rz7uZmbyvnUvsn7EJ2x/5q9uhsTtXbI3g=
|
||||
github.com/pion/sctp v1.8.9/go.mod h1:cMLT45jqw3+jiJCrtHVwfQLnfR0MGZ4rgOJwUOIqLkI=
|
||||
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
|
||||
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
||||
github.com/pion/srtp/v2 v2.0.16 h1:impT2XBrHKsDpXr1x5hHIRydwssrSWKpmw3KvSfXbso=
|
||||
github.com/pion/srtp/v2 v2.0.16/go.mod h1:NCLCV+U+NpxQ+vXhfOETet4OgKioIgrFjZmIM3ldJYE=
|
||||
github.com/pion/srtp/v2 v2.0.17 h1:ECuOk+7uIpY6HUlTb0nXhfvu4REG2hjtC4ronYFCZE4=
|
||||
github.com/pion/srtp/v2 v2.0.17/go.mod h1:y5WSHcJY4YfNB/5r7ca5YjHeIr1H3LM1rKArGGs8jMc=
|
||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
||||
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
|
||||
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
|
||||
github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
|
||||
github.com/pion/transport/v2 v2.2.0/go.mod h1:AdSw4YBZVDkZm8fpoz+fclXyQwANWmZAlDuQdctTThQ=
|
||||
github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c=
|
||||
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
||||
github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc=
|
||||
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo=
|
||||
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
|
||||
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
||||
github.com/pion/turn/v2 v2.1.3 h1:pYxTVWG2gpC97opdRc5IGsQ1lJ9O/IlNhkzj7MMrGAA=
|
||||
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/webrtc/v3 v3.2.17 h1:4ra4H3atxp02e891dz8ZOye2Rgfsv8E2VUksyS1EW28=
|
||||
github.com/pion/webrtc/v3 v3.2.17/go.mod h1:stMj0DIIhmUF0yOSR02uPAoKapzYbDIthSwW/Uk+AGs=
|
||||
github.com/pion/turn/v2 v2.1.4 h1:2xn8rduI5W6sCZQkEnIUDAkrBQNl2eYIBCHMZ3QMmP8=
|
||||
github.com/pion/turn/v2 v2.1.4/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/webrtc/v3 v3.2.19 h1:XNu5e62mkzafw1qYuKtQ+Dviw4JpbzC/SLx3zZt49JY=
|
||||
github.com/pion/webrtc/v3 v3.2.19/go.mod h1:vVURQTBOG5BpWKOJz3nlr23NfTDeyKVmubRNqzQp+Tg=
|
||||
github.com/pion/webrtc/v3 v3.2.21 h1:c8fy5JcqJkAQBwwy3Sk9huQLTBUSqaggyRlv9Lnh2zY=
|
||||
github.com/pion/webrtc/v3 v3.2.21/go.mod h1:vVURQTBOG5BpWKOJz3nlr23NfTDeyKVmubRNqzQp+Tg=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
|
||||
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
|
||||
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs=
|
||||
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
|
||||
@@ -106,7 +121,6 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
@@ -119,14 +133,20 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
|
||||
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@@ -136,14 +156,16 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -151,6 +173,7 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -170,41 +193,47 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
|
||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss=
|
||||
golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
|
||||
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
|
||||
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
+19
-1
@@ -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"`
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var Version = "1.7.0"
|
||||
var Version = "1.8.1"
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -71,7 +71,8 @@ func discovery() ([]*api.Source, error) {
|
||||
err := mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
|
||||
log.Trace().Msgf("[homekit] mdns=%s", entry)
|
||||
|
||||
if entry.Complete() && entry.Info[hap.TXTCategory] == hap.CategoryCamera {
|
||||
category := entry.Info[hap.TXTCategory]
|
||||
if entry.Complete() && (category == hap.CategoryCamera || category == hap.CategoryDoorbell) {
|
||||
source := &api.Source{
|
||||
Name: entry.Name,
|
||||
Info: entry.Info[hap.TXTModel],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+152
-3
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -62,7 +63,7 @@ func streamsHandler(rawURL string) (core.Producer, error) {
|
||||
// ex: ws://localhost:1984/api/ws?src=camera1
|
||||
func go2rtcClient(url string) (core.Producer, error) {
|
||||
// 1. Connect to signalign server
|
||||
conn, _, err := websocket.DefaultDialer.Dial(url, nil)
|
||||
conn, _, err := Dial(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -212,3 +213,27 @@ func whepClient(url string) (core.Producer, error) {
|
||||
|
||||
return prod, nil
|
||||
}
|
||||
|
||||
// Dial - websocket.Dial with Basic auth support
|
||||
func Dial(rawURL string) (*websocket.Conn, *http.Response, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if u.User == nil {
|
||||
return websocket.DefaultDialer.Dial(rawURL, nil)
|
||||
}
|
||||
|
||||
user := u.User.Username()
|
||||
pass, _ := u.User.Password()
|
||||
u.User = nil
|
||||
|
||||
header := http.Header{
|
||||
"Authorization": []string{
|
||||
"Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+pass)),
|
||||
},
|
||||
}
|
||||
|
||||
return websocket.DefaultDialer.Dial(u.String(), header)
|
||||
}
|
||||
|
||||
@@ -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:]
|
||||
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import (
|
||||
const (
|
||||
BufferSize = 64 * 1024 // 64K
|
||||
ConnDialTimeout = time.Second * 3
|
||||
ConnDeadline = time.Second * 3
|
||||
ConnDeadline = time.Second * 5
|
||||
ProbeTimeout = time.Second * 3
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+131
-322
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+247
-22
@@ -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)
|
||||
//}
|
||||
|
||||
+41
-2
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+4
-1
@@ -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")) {
|
||||
|
||||
@@ -139,3 +139,22 @@ func IndexFrame(b []byte) int {
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func FixAnnexBInAVCC(b []byte) []byte {
|
||||
for i := 0; i < len(b); {
|
||||
if i+4 >= len(b) {
|
||||
break
|
||||
}
|
||||
|
||||
size := bytes.Index(b[i+4:], []byte{0, 0, 0, 1})
|
||||
if size < 0 {
|
||||
size = len(b) - (i + 4)
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint32(b[i:], uint32(size))
|
||||
|
||||
i += size + 4
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
}
|
||||
|
||||
+2
-2
@@ -83,10 +83,10 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
|
||||
// some Chinese buggy cameras has single packet with SPS+PPS+IFrame separated by 00 00 00 01
|
||||
// https://github.com/AlexxIT/WebRTC/issues/391
|
||||
// https://github.com/AlexxIT/WebRTC/issues/392
|
||||
payload = annexb.EncodeToAVCC(payload, false)
|
||||
payload = annexb.FixAnnexBInAVCC(payload)
|
||||
}
|
||||
|
||||
//log.Printf("[AVC] %v, len: %d, ts: %10d, seq: %d", Types(payload), len(payload), packet.Timestamp, packet.SequenceNumber)
|
||||
//log.Printf("[AVC] %v, len: %d, ts: %10d, seq: %d", NALUTypes(payload), len(payload), packet.Timestamp, packet.SequenceNumber)
|
||||
|
||||
clone := *packet
|
||||
clone.Version = RTPPacketVersionAVC
|
||||
|
||||
+23
-3
@@ -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"`
|
||||
|
||||
+38
-6
@@ -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) {
|
||||
@@ -296,11 +323,13 @@ func (c *Client) PutCharacters(characters ...*Character) error {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = c.Put(PathCharacteristics, MimeJSON, bytes.NewReader(body))
|
||||
res, err := c.Put(PathCharacteristics, MimeJSON, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = io.ReadAll(res.Body) // important to "clear" body
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -317,8 +346,11 @@ func (c *Client) GetImage(width, height int) ([]byte, error) {
|
||||
}
|
||||
|
||||
func (c *Client) LocalIP() string {
|
||||
if c.Conn == nil {
|
||||
return ""
|
||||
}
|
||||
addr := c.Conn.LocalAddr().(*net.TCPAddr)
|
||||
return addr.IP.To4().String()
|
||||
return addr.IP.String()
|
||||
}
|
||||
|
||||
func DecodeKey(s string) []byte {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -156,6 +156,8 @@ func (c *Client) Pair(feature, pin string) (err error) {
|
||||
Proof string `tlv8:"4"` // server proof
|
||||
State byte `tlv8:"6"`
|
||||
Error byte `tlv8:"7"`
|
||||
|
||||
EncryptedData string `tlv8:"5"` // skip EncryptedData validation (for MFi devices)
|
||||
}
|
||||
if err = tlv8.UnmarshalReader(res.Body, &plainM4); err != nil {
|
||||
return
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+5
-3
@@ -31,8 +31,9 @@ const (
|
||||
StatusPaired = "0"
|
||||
StatusNotPaired = "1"
|
||||
|
||||
CategoryBridge = "2"
|
||||
CategoryCamera = "17"
|
||||
CategoryBridge = "2"
|
||||
CategoryCamera = "17"
|
||||
CategoryDoorbell = "18"
|
||||
|
||||
StateM1 = 1
|
||||
StateM2 = 2
|
||||
@@ -65,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) {
|
||||
|
||||
+49
-57
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
@@ -175,10 +175,10 @@ func (c *Client) Start() error {
|
||||
func (c *Client) Stop() error {
|
||||
_ = c.SuperProducer.Close()
|
||||
|
||||
if c.videoSession != nil {
|
||||
if c.videoSession != nil && c.videoSession.Remote != nil {
|
||||
c.srtp.DelSession(c.videoSession)
|
||||
}
|
||||
if c.audioSession != nil {
|
||||
if c.audioSession != nil && c.audioSession.Remote != nil {
|
||||
c.srtp.DelSession(c.audioSession)
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
+16
-9
@@ -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
-4
@@ -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 ""
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
+10
-7
@@ -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 != "" {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 ""
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
+1
-1
@@ -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)
|
||||
}
|
||||
|
||||
+5
-5
@@ -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()
|
||||
|
||||
+12
-3
@@ -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
|
||||
}
|
||||
|
||||
@@ -109,6 +109,13 @@ func AddOrReplace(src []byte, key string, value any, nodeParent *yaml.Node) ([]b
|
||||
i0 := LineOffset(src, nodeKey.Line)
|
||||
i1 := LineOffset(src, LastChild(nodeValue).Line+1)
|
||||
|
||||
if i1 < 0 { // no new line on the end of file
|
||||
if value != nil {
|
||||
return append(src[:i0], put...), nil
|
||||
}
|
||||
return src[:i0], nil
|
||||
}
|
||||
|
||||
dst := make([]byte, 0, len(src)+len(put))
|
||||
dst = append(dst, src[:i0]...)
|
||||
if value != nil {
|
||||
@@ -121,6 +128,14 @@ func AddOrReplace(src []byte, key string, value any, nodeParent *yaml.Node) ([]b
|
||||
|
||||
i := LineOffset(src, LastChild(nodeParent).Line+1)
|
||||
|
||||
if i < 0 { // no new line on the end of file
|
||||
src = append(src, '\n')
|
||||
if value != nil {
|
||||
src = append(src, put...)
|
||||
}
|
||||
return src, nil
|
||||
}
|
||||
|
||||
dst := make([]byte, 0, len(src)+len(put))
|
||||
dst = append(dst, src[:i]...)
|
||||
if value != nil {
|
||||
|
||||
@@ -104,3 +104,43 @@ func TestPatch2(t *testing.T) {
|
||||
camera2: url3
|
||||
`, string(b))
|
||||
}
|
||||
|
||||
func TestNoNewLineEnd1(t *testing.T) {
|
||||
b := []byte(`streams:
|
||||
camera1: url4
|
||||
camera2:
|
||||
- url2
|
||||
- url3`)
|
||||
|
||||
b, err := Patch(b, "camera2", "url5", "streams")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, `streams:
|
||||
camera1: url4
|
||||
camera2: url5
|
||||
`, string(b))
|
||||
}
|
||||
|
||||
func TestNoNewLineEnd2(t *testing.T) {
|
||||
b := []byte(`streams:
|
||||
camera1: url1
|
||||
homekit:
|
||||
camera1:
|
||||
pin: 123-45-678`)
|
||||
|
||||
// 1. Add new key
|
||||
pairings := []string{"client1", "client2"}
|
||||
|
||||
b, err := Patch(b, "pairings", pairings, "homekit", "camera1")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, `streams:
|
||||
camera1: url1
|
||||
homekit:
|
||||
camera1:
|
||||
pin: 123-45-678
|
||||
pairings:
|
||||
- client1
|
||||
- client2
|
||||
`, string(b))
|
||||
}
|
||||
|
||||
+30
-27
@@ -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;
|
||||
@@ -29,38 +29,41 @@
|
||||
<br>
|
||||
<div id="config"></div>
|
||||
<script>
|
||||
ace.config.set('basePath', 'https://cdnjs.cloudflare.com/ajax/libs/ace/1.24.1/');
|
||||
let dump;
|
||||
|
||||
ace.config.set('basePath', 'https://unpkg.com/ace-builds@1.28.0/src-min/');
|
||||
const editor = ace.edit('config', {
|
||||
mode: 'ace/mode/yaml',
|
||||
});
|
||||
|
||||
document.getElementById('save').addEventListener('click', () => {
|
||||
fetch('api/config', {
|
||||
method: 'POST', body: editor.getValue()
|
||||
}).then(r => {
|
||||
if (r.ok) {
|
||||
alert('OK');
|
||||
fetch('api/exit?code=100', {method: 'POST'});
|
||||
} else {
|
||||
r.text().then(alert);
|
||||
}
|
||||
});
|
||||
document.getElementById('save').addEventListener('click', async () => {
|
||||
let r = await fetch('api/config', {cache: 'no-cache'});
|
||||
if (r.ok && dump !== await r.text()) {
|
||||
alert('Config was changed from another place. Refresh the page and make changes again');
|
||||
return;
|
||||
}
|
||||
|
||||
r = await fetch('api/config', {method: 'POST', body: editor.getValue()});
|
||||
if (r.ok) {
|
||||
alert('OK');
|
||||
fetch('api/restart', {method: 'POST'});
|
||||
} else {
|
||||
alert(await r.text());
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
fetch('api/config', {cache: 'no-cache'}).then(r => {
|
||||
if (r.status === 410) {
|
||||
alert('Config file is not set');
|
||||
} else if (r.status === 404) {
|
||||
editor.setValue(''); // config file not exist
|
||||
} else if (r.ok) {
|
||||
r.text().then(data => {
|
||||
editor.setValue(data);
|
||||
});
|
||||
} else {
|
||||
alert(`Unknown error: ${r.statusText} (${r.status})`);
|
||||
}
|
||||
});
|
||||
window.addEventListener('load', async () => {
|
||||
const r = await fetch('api/config', {cache: 'no-cache'});
|
||||
if (r.status === 410) {
|
||||
alert('Config file is not set');
|
||||
} else if (r.status === 404) {
|
||||
editor.setValue(''); // config file not exist
|
||||
} else if (r.ok) {
|
||||
dump = await r.text();
|
||||
editor.setValue(dump);
|
||||
} else {
|
||||
alert(`Unknown error: ${r.statusText} (${r.status})`);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
+21
-4
@@ -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>
|
||||
|
||||
+85
-56
@@ -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