diff --git a/README.md b/README.md index 4c45bdd0..f69425ec 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Ultimate camera streaming application with support for RTSP, WebRTC, HomeKit, FF - creator of the project's logo [@v_novoseltsev](https://www.instagram.com/v_novoseltsev) > [!CAUTION] -> There is NO existing website for go2rtc project other than this GitHub repository. The website go2rtc[.]com is in no way associated with the authors of this project. +> The official website of the project is this GitHub repository and go2rtc.org (hosted on GitHub Pages). The website go2rtc[.]com is in no way associated with the authors of this project. --- @@ -378,9 +378,11 @@ But you can override them via YAML config. You can also add your own formats to ```yaml ffmpeg: bin: ffmpeg # path to ffmpeg binary + global: "-hide_banner" + timeout: 5 # default timeout in seconds for rtsp inputs h264: "-codec:v libx264 -g:v 30 -preset:v superfast -tune:v zerolatency -profile:v main -level:v 4.1" mycodec: "-any args that supported by ffmpeg..." - myinput: "-fflags nobuffer -flags low_delay -timeout 5000000 -i {input}" + myinput: "-fflags nobuffer -flags low_delay -timeout {timeout} -i {input}" myraw: "-ss 00:00:20" ``` @@ -390,9 +392,10 @@ ffmpeg: - You can use `width` and/or `height` params, important with transcoding (ex. `#video=h264#width=1280`) - You can use `drawtext` to add a timestamp (ex. `drawtext=x=2:y=2:fontsize=12:fontcolor=white:box=1:boxcolor=black`) - This will greatly increase the CPU of the server, even with hardware acceleration +- You can use `timeout` param to set RTSP input timeout in seconds (ex. `#timeout=10`) - You can use `raw` param for any additional FFmpeg arguments (ex. `#raw=-vf transpose=1`) - You can use `input` param to override default input template (ex. `#input=rtsp/udp` will change RTSP transport from TCP to UDP+TCP) - - You can use raw input value (ex. `#input=-timeout 5000000 -i {input}`) + - You can use raw input value (ex. `#input=-timeout {timeout} -i {input}`) - You can add your own input templates Read more about [hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration). @@ -1122,7 +1125,7 @@ webtorrent: src: rtsp-dahua1 # stream name from streams section ``` -Link example: https://alexxit.github.io/go2rtc/#share=02SNtgjKXY&pwd=wznEQqznxW&media=video+audio +Link example: https://go2rtc.org/webtorrent/#share=02SNtgjKXY&pwd=wznEQqznxW&media=video+audio ### Module: ngrok diff --git a/api/README.md b/api/README.md index 76dc875b..03b99bc4 100644 --- a/api/README.md +++ b/api/README.md @@ -4,7 +4,7 @@ Fill free to make any API design proposals. ## HTTP API -Interactive [OpenAPI](https://alexxit.github.io/go2rtc/api/). +Interactive [OpenAPI](https://go2rtc.org/api/). `www/stream.html` - universal viewer with support params in URL: diff --git a/api/openapi.yaml b/api/openapi.yaml index 6ec0b492..b6110572 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -2,8 +2,8 @@ openapi: 3.1.0 info: title: go2rtc + version: 1.9.13 license: { name: MIT,url: https://opensource.org/licenses/MIT } - version: 1.0.0 contact: { url: https://github.com/AlexxIT/go2rtc } description: | Ultimate camera streaming application with support RTSP, RTMP, HTTP-FLV, WebRTC, MSE, HLS, MP4, MJPEG, HomeKit, FFmpeg, etc. @@ -11,6 +11,28 @@ info: servers: - url: http://localhost:1984 +tags: + - name: Application + description: "[Module: API](https://github.com/AlexxIT/go2rtc#module-api)" + - name: Config + description: "[Configuration](https://github.com/AlexxIT/go2rtc#configuration)" + - name: Streams list + description: "[Module: Streams](https://github.com/AlexxIT/go2rtc#module-streams)" + - name: Consume stream + - name: HLS + - name: Snapshot + - name: Produce stream + - name: WebSocket + description: "WebSocket API endpoint: `/api/ws` (see `api/README.md`)" + - name: Discovery + - name: HomeKit + - name: ONVIF + - name: RTSPtoWebRTC + - name: WebTorrent + description: "[Module: WebTorrent](https://github.com/AlexxIT/go2rtc#module-webtorrent)" + - name: FFmpeg + - name: Debug + components: parameters: stream_src_path: @@ -20,6 +42,7 @@ components: required: true schema: { type: string } example: camera1 + stream_dst_path: name: dst in: path @@ -27,6 +50,7 @@ components: required: true schema: { type: string } example: camera1 + stream_src_query: name: src in: query @@ -34,6 +58,15 @@ components: required: true schema: { type: string } example: camera1 + + hls_session_id_path: + name: id + in: path + description: HLS session ID (passed as query param `id`) + required: true + schema: { type: string } + example: DvmHdd9w + mp4_filter: name: mp4 in: query @@ -43,6 +76,7 @@ components: type: string enum: [ "", flac, all ] example: flac + video_filter: name: video in: query @@ -51,6 +85,7 @@ components: type: string enum: [ "", all, h264, h265, mjpeg ] example: h264,h265 + audio_filter: name: audio in: query @@ -59,35 +94,20 @@ components: type: string enum: [ "", all, aac, opus, pcm, pcmu, pcma ] example: aac + responses: discovery: description: "" content: application/json: example: { streams: [ { "name": "Camera 1","url": "..." } ] } + webtorrent: description: "" content: application/json: example: { share: AKDypPy4zz, pwd: H0Km1HLTTP } -tags: - - name: Application - description: "[Module: API](https://github.com/AlexxIT/go2rtc#module-api)" - - name: Config - description: "[Configuration](https://github.com/AlexxIT/go2rtc#configuration)" - - name: Streams list - description: "[Module: Streams](https://github.com/AlexxIT/go2rtc#module-streams)" - - name: Consume stream - - name: Snapshot - - name: Produce stream - - name: Discovery - - name: ONVIF - - name: RTSPtoWebRTC - - name: WebTorrent - description: "[Module: WebTorrent](https://github.com/AlexxIT/go2rtc#module-webtorrent)" - - name: Debug - paths: /api: get: @@ -98,7 +118,17 @@ paths: description: "" content: application/json: - example: { config_path: "/config/go2rtc.yaml",host: "192.168.1.123:1984",rtsp: { listen: ":8554",default_query: "video&audio" },version: "1.5.0" } + schema: + type: object + properties: + config_path: { type: string, example: "/config/go2rtc.yaml" } + host: { type: string, example: "192.168.1.123:1984" } + rtsp: + type: object + properties: + listen: { type: string, example: ":8554" } + default_query: { type: string, example: "video&audio" } + version: { type: string, example: "1.9.12" } /api/exit: post: @@ -112,17 +142,39 @@ paths: schema: { type: integer } example: 100 responses: - default: - description: Default response + default: + description: "" /api/restart: post: - summary: Restart Daemon + summary: Restart daemon description: Restarts the daemon. tags: [ Application ] responses: - default: - description: Default response + default: + description: "" + + /api/log: + get: + summary: Get in-memory logs buffer + description: | + Returns current log output from the in-memory circular buffer. + tags: [ Application ] + responses: + "200": + description: OK + content: + application/jsonlines: + example: | + {"level":"info","version":"1.9.13","platform":"linux/amd64","revision":"dfe4755","time":1766841087331,"message":"go2rtc"} + delete: + summary: Clear in-memory logs buffer + tags: [ Application ] + responses: + "200": + description: "" + content: + text/plain: { example: "" } /api/config: get: @@ -133,6 +185,8 @@ paths: description: "" content: application/yaml: { example: "streams:..." } + "404": + description: Config file not found post: summary: Rewrite main config file tags: [ Config ] @@ -140,8 +194,8 @@ paths: content: "*/*": { example: "streams:..." } responses: - default: - description: Default response + default: + description: "" patch: summary: Merge changes to main config file tags: [ Config ] @@ -149,8 +203,8 @@ paths: content: "*/*": { example: "streams:..." } responses: - default: - description: Default response + default: + description: "" @@ -162,7 +216,16 @@ paths: "200": description: "" content: - application/json: { example: { camera1: { producers: [ ],consumers: [ ] } } } + application/json: + schema: + type: object + additionalProperties: + type: object + properties: + producers: + type: array + consumers: + type: array put: summary: Create new stream tags: [ Streams list ] @@ -180,8 +243,8 @@ paths: schema: { type: string } example: camera1 responses: - default: - description: Default response + default: + description: "" patch: summary: Update stream source tags: [ Streams list ] @@ -199,8 +262,8 @@ paths: schema: { type: string } example: camera1 responses: - default: - description: Default response + default: + description: "" delete: summary: Delete stream tags: [ Streams list ] @@ -212,8 +275,8 @@ paths: schema: { type: string } example: camera1 responses: - default: - description: Default response + default: + description: "" post: summary: Send stream from source to destination description: "[Stream to camera](https://github.com/AlexxIT/go2rtc#stream-to-camera)" @@ -232,10 +295,26 @@ paths: schema: { type: string } example: camera1 responses: - default: - description: Default response - + default: + description: "" + /api/streams.dot: + get: + summary: Get streams graph in Graphviz DOT format + tags: [ Streams list ] + parameters: + - name: src + in: query + description: Stream name filter. Repeat `src` to include multiple streams. + required: false + schema: { type: string } + example: camera1 + responses: + "200": + description: OK + content: + text/vnd.graphviz: + example: "digraph { ... }" /api/preload: get: @@ -245,7 +324,17 @@ paths: "200": description: "" content: - application/json: { example: { camera1: "video&audio", camera2: "video" } } + application/json: + schema: + type: object + additionalProperties: + type: object + properties: + consumer: + type: object + query: + type: string + example: "video&audio" put: summary: Preload new stream tags: [ Streams list ] @@ -275,8 +364,8 @@ paths: schema: { type: string } example: all,aac,opus,... responses: - default: - description: Default response + default: + description: "" delete: summary: Delete preloaded stream tags: [ Streams list ] @@ -288,9 +377,22 @@ paths: schema: { type: string } example: "camera1" responses: - default: - description: Default response + default: + description: "" + /api/schemes: + get: + summary: Get supported source URL schemes + tags: [ Streams list ] + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: { type: string } + example: [ rtsp, rtmp, webrtc, ffmpeg, hass ] /api/streams?src={src}: @@ -304,7 +406,17 @@ paths: description: "" content: application/json: - example: { producers: [ { url: "rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0" } ], consumers: [ ] } + schema: + type: object + additionalProperties: + type: object + properties: + producers: + type: array + items: { type: object } + consumers: + type: array + items: { type: object } /api/webrtc?src={src}: post: @@ -324,7 +436,6 @@ paths: application/json: { example: { type: offer, sdp: "v=0..." } } "application/sdp": { example: "v=0..." } "*/*": { example: "v=0..." } - responses: "200": description: "Response on JSON or raw SDP" @@ -355,6 +466,16 @@ paths: required: false schema: { type: string } example: camera1.mp4 + - name: rotate + in: query + description: "Rotate video (degrees). Supported values: 90, 180, 270." + required: false + schema: { type: integer, enum: [ 90, 180, 270 ] } + - name: scale + in: query + description: Scale video in format `width:height` + required: false + schema: { type: string, example: "1280:720" } - $ref: "#/components/parameters/mp4_filter" - $ref: "#/components/parameters/video_filter" - $ref: "#/components/parameters/audio_filter" @@ -367,7 +488,7 @@ paths: get: summary: Get stream in HLS format description: "[Module: HLS](https://github.com/AlexxIT/go2rtc#module-hls)" - tags: [ Consume stream ] + tags: [ Consume stream, HLS ] parameters: - $ref: "#/components/parameters/stream_src_path" - $ref: "#/components/parameters/mp4_filter" @@ -378,6 +499,62 @@ paths: description: "" content: { application/vnd.apple.mpegurl: { example: "" } } + /api/hls/playlist.m3u8?id={id}: + get: + summary: Get HLS media playlist for an active session + tags: [ HLS ] + parameters: + - $ref: "#/components/parameters/hls_session_id_path" + responses: + "200": + description: OK + content: + application/vnd.apple.mpegurl: { example: "" } + "404": + description: Session not found + + /api/hls/segment.ts?id={id}: + get: + summary: Get HLS MPEG-TS segment for an active session + tags: [ HLS ] + parameters: + - $ref: "#/components/parameters/hls_session_id_path" + responses: + "200": + description: OK + content: + video/mp2t: { example: "" } + "404": + description: Segment or session not found + + /api/hls/init.mp4?id={id}: + get: + summary: Get HLS fMP4 init segment for an active session + tags: [ HLS ] + parameters: + - $ref: "#/components/parameters/hls_session_id_path" + responses: + "200": + description: OK + content: + video/mp4: { example: "" } + "404": + description: Segment or session not found + + /api/hls/segment.m4s?id={id}: + get: + summary: Get HLS fMP4 media segment for an active session + tags: [ HLS ] + parameters: + - $ref: "#/components/parameters/hls_session_id_path" + responses: + "200": + description: OK + content: + video/iso.segment: { example: "" } + "404": + description: Segment or session not found + /api/stream.mjpeg?src={src}: get: summary: Get stream in MJPEG format @@ -390,7 +567,91 @@ paths: description: "" content: { multipart/x-mixed-replace: { example: "" } } + /api/stream.ascii?src={src}: + get: + summary: Get stream in ASCII-art format (ANSI escape codes) + description: "[Module: MJPEG](https://github.com/AlexxIT/go2rtc#module-mjpeg)" + tags: [ Consume stream ] + parameters: + - $ref: "#/components/parameters/stream_src_path" + - name: color + in: query + description: Foreground mode (`8`, `256`, `rgb` or ANSI SGR code) + required: false + schema: { type: string } + - name: back + in: query + description: Background mode (`8`, `256`, `rgb` or ANSI SGR code) + required: false + schema: { type: string } + - name: text + in: query + description: Charset preset (empty/default, `block`) or custom characters + required: false + schema: { type: string } + responses: + "200": + description: OK + content: + text/plain: { example: "" } + "404": + description: Stream not found + /api/stream.y4m?src={src}: + get: + summary: Get stream in YUV4MPEG2 format (y4m) + tags: [ Consume stream ] + parameters: + - $ref: "#/components/parameters/stream_src_path" + responses: + "200": + description: OK + content: + application/octet-stream: { example: "" } + "404": + description: Stream not found + + /api/stream.ts?src={src}: + get: + summary: Get stream in MPEG-TS format + tags: [ Consume stream ] + parameters: + - $ref: "#/components/parameters/stream_src_path" + responses: + "200": + description: OK + content: + video/mp2t: { example: "" } + "404": + description: Stream not found + + /api/stream.aac?src={src}: + get: + summary: Get stream audio in AAC (ADTS) format + tags: [ Consume stream ] + parameters: + - $ref: "#/components/parameters/stream_src_path" + responses: + "200": + description: OK + content: + audio/aac: { example: "" } + "404": + description: Stream not found + + /api/stream.flv?src={src}: + get: + summary: Get stream in FLV format + tags: [ Consume stream ] + parameters: + - $ref: "#/components/parameters/stream_src_path" + responses: + "200": + description: OK + content: + video/x-flv: { example: "" } + "404": + description: Stream not found /api/frame.jpeg?src={src}: get: @@ -399,10 +660,37 @@ paths: tags: [ Snapshot ] parameters: - $ref: "#/components/parameters/stream_src_path" + - name: name + in: query + description: Optional stream name to create/update if `src` is a URL + required: false + schema: { type: string } + - name: width + in: query + description: "Scale output width (alias: `w`)" + required: false + schema: { type: integer, minimum: 1 } + - name: height + in: query + description: "Scale output height (alias: `h`)" + required: false + schema: { type: integer, minimum: 1 } + - name: rotate + in: query + description: "Rotate output (degrees). Supported values: 90, 180, 270." + required: false + schema: { type: integer, enum: [ 90, 180, 270 ] } + - name: hardware + in: query + description: "Hardware acceleration engine for FFmpeg snapshot transcoding (alias: `hw`)" + required: false + schema: { type: string } responses: - 200: + "200": description: "" - content: { image/jpeg: { example: "" } } + content: + image/jpeg: { example: "" } + /api/frame.mp4?src={src}: get: summary: Get snapshot in MP4 format @@ -410,23 +698,51 @@ paths: tags: [ Snapshot ] parameters: - $ref: "#/components/parameters/stream_src_path" + - name: filename + in: query + description: Download as a file with this name + required: false + schema: { type: string } + example: camera1.mp4 responses: 200: description: "" - content: { video/mp4: { example: "" } } + content: + video/mp4: { example: "" } /api/webrtc?dst={dst}: post: - summary: Post stream in WebRTC format + summary: Post stream in WebRTC format (WHIP) description: "[Incoming: WebRTC/WHIP](https://github.com/AlexxIT/go2rtc#incoming-webrtcwhip)" tags: [ Produce stream ] parameters: - $ref: "#/components/parameters/stream_dst_path" responses: - default: - description: Default response + "201": + description: Created + headers: + Location: + description: Resource URL for session + schema: { type: string } + content: + application/sdp: { example: "v=0..." } + "404": + description: Stream not found + + /api/stream?dst={dst}: + post: + summary: Post stream in auto-detected format + description: | + Incoming source with automatic format detection. Use for pushing a stream into an existing `dst` stream. + tags: [ Produce stream ] + parameters: + - $ref: "#/components/parameters/stream_dst_path" + responses: + default: + description: "" + /api/stream.flv?dst={dst}: post: summary: Post stream in FLV format @@ -435,8 +751,9 @@ paths: parameters: - $ref: "#/components/parameters/stream_dst_path" responses: - default: - description: Default response + default: + description: "" + /api/stream.ts?dst={dst}: post: summary: Post stream in MPEG-TS format @@ -445,8 +762,9 @@ paths: parameters: - $ref: "#/components/parameters/stream_dst_path" responses: - default: - description: Default response + default: + description: "" + /api/stream.mjpeg?dst={dst}: post: summary: Post stream in MJPEG format @@ -455,10 +773,55 @@ paths: parameters: - $ref: "#/components/parameters/stream_dst_path" responses: - default: - description: Default response + default: + description: "" + /api/ffmpeg: + post: + summary: Play file/live/TTS into a stream via FFmpeg + description: | + Helper endpoint for "stream to camera" scenarios. + Exactly one of `file`, `live`, `text` should be provided. + tags: [ FFmpeg ] + parameters: + - name: dst + in: query + description: Destination stream name + required: true + schema: { type: string } + example: camera1 + - name: file + in: query + description: Input URL to treat as file (`#input=file`) + required: false + schema: { type: string } + example: "http://example.com/song.mp3" + - name: live + in: query + description: Live input URL + required: false + schema: { type: string } + example: "http://example.com/live.mp3" + - name: text + in: query + description: Text-to-speech phrase + required: false + schema: { type: string } + example: "Hello" + - name: voice + in: query + description: Optional TTS voice (engine-dependent) + required: false + schema: { type: string } + responses: + "200": + description: OK + "400": + description: Invalid parameters + "404": + description: Stream not found + /api/dvrip: get: @@ -466,8 +829,7 @@ paths: description: "[Source: DVRIP](https://github.com/AlexxIT/go2rtc#source-dvrip)" tags: [ Discovery ] responses: - default: - description: Default response + "200": { $ref: "#/components/responses/discovery" } /api/ffmpeg/devices: get: @@ -475,56 +837,275 @@ paths: description: "[Source: FFmpeg Device](https://github.com/AlexxIT/go2rtc#source-ffmpeg-device)" tags: [ Discovery ] responses: - default: - description: Default response + "200": { $ref: "#/components/responses/discovery" } + /api/ffmpeg/hardware: get: summary: FFmpeg hardware transcoding discovery description: "[Hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration)" tags: [ Discovery ] responses: - default: - description: Default response + "200": { $ref: "#/components/responses/discovery" } + + /api/v4l2: + get: + summary: V4L2 video devices discovery (Linux) + tags: [ Discovery ] + responses: + "200": { $ref: "#/components/responses/discovery" } + + /api/alsa: + get: + summary: ALSA audio devices discovery (Linux) + tags: [ Discovery ] + responses: + "200": { $ref: "#/components/responses/discovery" } + + /api/gopro: + get: + summary: GoPro cameras discovery + tags: [ Discovery ] + responses: + "200": { $ref: "#/components/responses/discovery" } + + /api/ring: + get: + summary: Ring cameras discovery + description: | + Provide either `email`/`password` (and optional `code` for 2FA) or `refresh_token`. + If 2FA is required, returns a JSON prompt instead of sources. + tags: [ Discovery ] + parameters: + - name: email + in: query + required: false + schema: { type: string } + - name: password + in: query + required: false + schema: { type: string } + - name: code + in: query + required: false + schema: { type: string } + - name: refresh_token + in: query + required: false + schema: { type: string } + responses: + "200": + description: OK + content: + application/json: { example: "" } + + /api/tuya: + get: + summary: Tuya cameras discovery + tags: [ Discovery ] + parameters: + - name: region + in: query + description: Tuya API host (region) + required: true + schema: { type: string } + example: "openapi.tuyaus.com" + - name: email + in: query + required: true + schema: { type: string } + - name: password + in: query + required: true + schema: { type: string } + responses: + "200": { $ref: "#/components/responses/discovery" } + "400": + description: Invalid parameters + "404": + description: No cameras found + /api/hass: get: summary: Home Assistant cameras discovery description: "[Source: Hass](https://github.com/AlexxIT/go2rtc#source-hass)" tags: [ Discovery ] responses: - default: - description: Default response - /api/homekit: + "200": { $ref: "#/components/responses/discovery" } + "404": { description: No Hass config } + + /api/discovery/homekit: get: summary: HomeKit cameras discovery description: "[Source: HomeKit](https://github.com/AlexxIT/go2rtc#source-homekit)" tags: [ Discovery ] responses: - default: - description: Default response + "200": { $ref: "#/components/responses/discovery" } + /api/nest: get: summary: Nest cameras discovery tags: [ Discovery ] + parameters: + - name: client_id + in: query + required: true + schema: { type: string } + - name: client_secret + in: query + required: true + schema: { type: string } + - name: refresh_token + in: query + required: true + schema: { type: string } + - name: project_id + in: query + required: true + schema: { type: string } responses: - default: - description: Default response + "200": { $ref: "#/components/responses/discovery" } + /api/onvif: get: summary: ONVIF cameras discovery description: "[Source: ONVIF](https://github.com/AlexxIT/go2rtc#source-onvif)" tags: [ Discovery ] + parameters: + - name: src + in: query + description: Optional ONVIF device URL to enumerate profiles + required: false + schema: { type: string } + example: "onvif://user:pass@192.168.1.50:80" responses: - default: - description: Default response + "200": { $ref: "#/components/responses/discovery" } + /api/roborock: get: - summary: Roborock vacuums discovery + summary: Roborock vacuums discovery (requires prior auth) description: "[Source: Roborock](https://github.com/AlexxIT/go2rtc#source-roborock)" tags: [ Discovery ] responses: - default: - description: Default response + "200": { $ref: "#/components/responses/discovery" } + "404": + description: No auth + post: + summary: Roborock login and discovery + tags: [ Discovery ] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + username: { type: string } + password: { type: string } + required: [ username, password ] + responses: + "200": { $ref: "#/components/responses/discovery" } + /api/homekit: + get: + summary: Get HomeKit servers state + tags: [ HomeKit ] + parameters: + - name: id + in: query + description: Optional stream name (server ID) + required: false + schema: { type: string } + responses: + "200": + description: OK + content: + application/json: { example: "" } + "404": + description: Server not found + post: + summary: Pair HomeKit camera and create/update stream + tags: [ HomeKit ] + parameters: + - name: id + in: query + description: Stream name to create/update + required: true + schema: { type: string } + - name: src + in: query + description: HomeKit URL (without pin) + required: true + schema: { type: string } + - name: pin + in: query + description: HomeKit PIN + required: true + schema: { type: string } + responses: + "200": + description: OK + delete: + summary: Unpair HomeKit camera and delete stream + tags: [ HomeKit ] + parameters: + - name: id + in: query + description: Stream name / server ID + required: true + schema: { type: string } + responses: + "200": + description: OK + "404": + description: Stream not found + + /api/homekit/accessories: + get: + summary: Get HomeKit accessories JSON for a stream + tags: [ HomeKit ] + parameters: + - name: id + in: query + description: Stream name + required: true + schema: { type: string } + responses: + "200": + description: OK + content: + application/json: { example: { } } + "404": + description: Stream not found + + /pair-setup: + post: + summary: HomeKit Pair Setup (HAP) + description: HomeKit Accessory Protocol endpoint (TLV8). + tags: [ HomeKit ] + requestBody: + required: true + content: + application/pairing+tlv8: { example: "" } + responses: + "200": + description: OK + content: + application/pairing+tlv8: { example: "" } + + /pair-verify: + post: + summary: HomeKit Pair Verify (HAP) + description: HomeKit Accessory Protocol endpoint (TLV8). + tags: [ HomeKit ] + requestBody: + required: true + content: + application/pairing+tlv8: { example: "" } + responses: + "200": + description: OK + content: + application/pairing+tlv8: { example: "" } /onvif/: @@ -533,8 +1114,8 @@ paths: description: Simple realisation of the ONVIF protocol. Accepts any suburl requests tags: [ ONVIF ] responses: - default: - description: Default response + default: + description: "" @@ -544,8 +1125,33 @@ paths: description: Simple API for support [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration tags: [ RTSPtoWebRTC ] responses: - default: - description: Default response + default: + description: "" + + + /api/ws: + get: + summary: WebSocket endpoint + description: | + Upgrade to WebSocket and exchange JSON messages: + - Request: `{ "type": "...", "value": ... }` + - Response: `{ "type": "...", "value": ... }` + + Supported message types depend on enabled modules (see `api/README.md`). + tags: [ WebSocket ] + parameters: + - name: src + in: query + description: Stream name (consumer) + required: false + schema: { type: string } + - name: dst + in: query + description: Stream name (producer) + required: false + schema: { type: string } + responses: + "101": { description: Switching Protocols } @@ -564,15 +1170,13 @@ paths: - $ref: "#/components/parameters/stream_src_path" responses: 200: { $ref: "#/components/responses/webtorrent" } - delete: summary: Delete WebTorrent share tags: [ WebTorrent ] parameters: - $ref: "#/components/parameters/stream_src_path" responses: - default: - description: Default response + default: { description: "" } /api/webtorrent: get: diff --git a/internal/app/log.go b/internal/app/log.go index 9ec89a2c..61fd474c 100644 --- a/internal/app/log.go +++ b/internal/app/log.go @@ -14,6 +14,8 @@ import ( var MemoryLog = newBuffer() func GetLogger(module string) zerolog.Logger { + Logger.Trace().Str("module", module).Msgf("[log] init") + if s, ok := modules[module]; ok { lvl, err := zerolog.ParseLevel(s) if err == nil { diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index 242c151d..a3d589b1 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -58,15 +58,15 @@ func Init() { } var defaults = map[string]string{ - "bin": "ffmpeg", - "global": "-hide_banner", + "bin": "ffmpeg", + "global": "-hide_banner", + "timeout": "5", // inputs - "file": "-re -i {input}", - "http": "-fflags nobuffer -flags low_delay -i {input}", - "rtsp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i {input}", - - "rtsp/udp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i {input}", + "file": "-re -i {input}", + "http": "-fflags nobuffer -flags low_delay -i {input}", + "rtsp": "-fflags nobuffer -flags low_delay -timeout {timeout} -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i {input}", + "rtsp/udp": "-fflags nobuffer -flags low_delay -timeout {timeout} -user_agent go2rtc/ffmpeg -i {input}", // output "output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", @@ -169,6 +169,13 @@ func inputTemplate(name, s string, query url.Values) string { } else { template = defaults[name] } + if strings.Contains(template, "{timeout}") { + timeout := query.Get("timeout") + if timeout == "" { + timeout = defaults["timeout"] + } + template = strings.Replace(template, "{timeout}", timeout+"000000", 1) + } return strings.Replace(template, "{input}", s, 1) } diff --git a/internal/ffmpeg/ffmpeg_test.go b/internal/ffmpeg/ffmpeg_test.go index 30052d78..b9d02183 100644 --- a/internal/ffmpeg/ffmpeg_test.go +++ b/internal/ffmpeg/ffmpeg_test.go @@ -123,6 +123,11 @@ func TestParseArgsIpCam(t *testing.T) { source: "rtmp://example.com#input=rtsp/udp", expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtmp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, }, + { + name: "[RTSP] custom timeout", + source: "rtsp://example.com#timeout=10", + expect: `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 10000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { diff --git a/internal/onvif/onvif.go b/internal/onvif/onvif.go index 3c64cb5c..8d80398a 100644 --- a/internal/onvif/onvif.go +++ b/internal/onvif/onvif.go @@ -7,6 +7,7 @@ import ( "net/url" "os" "strconv" + "strings" "time" "github.com/AlexxIT/go2rtc/internal/api" @@ -43,6 +44,11 @@ func streamOnvif(rawURL string) (core.Producer, error) { return nil, err } + // Append hash-based arguments to the retrieved URI + if i := strings.IndexByte(rawURL, '#'); i > 0 { + uri += rawURL[i:] + } + log.Debug().Msgf("[onvif] new uri=%s", uri) if err = streams.Validate(uri); err != nil { diff --git a/internal/webrtc/webrtc.go b/internal/webrtc/webrtc.go index 03545cff..2a5b4ad6 100644 --- a/internal/webrtc/webrtc.go +++ b/internal/webrtc/webrtc.go @@ -2,6 +2,7 @@ package webrtc import ( "errors" + "net" "strings" "github.com/AlexxIT/go2rtc/internal/api" @@ -33,7 +34,13 @@ func Init() { log = app.GetLogger("webrtc") - filters = cfg.Mod.Filters + if log.Debug().Enabled() { + itfs, _ := net.Interfaces() + for _, itf := range itfs { + addrs, _ := itf.Addrs() + log.Debug().Msgf("[webrtc] interface %+v addrs %v", itf, addrs) + } + } address, network, _ := strings.Cut(cfg.Mod.Listen, "/") for _, candidate := range cfg.Mod.Candidates { @@ -50,10 +57,19 @@ func Init() { } } + webrtc.OnNewListener = func(ln any) { + switch ln := ln.(type) { + case *net.TCPListener: + log.Info().Stringer("addr", ln.Addr()).Msg("[webrtc] listen tcp") + case *net.UDPConn: + log.Info().Stringer("addr", ln.LocalAddr()).Msg("[webrtc] listen udp") + } + } + var err error // create pionAPI with custom codecs list and custom network settings - serverAPI, err = webrtc.NewServerAPI(network, address, &filters) + serverAPI, err = webrtc.NewServerAPI(network, address, &cfg.Mod.Filters) if err != nil { log.Error().Err(err).Caller().Send() return @@ -63,7 +79,6 @@ func Init() { clientAPI = serverAPI if address != "" { - log.Info().Str("addr", cfg.Mod.Listen).Msg("[webrtc] listen") clientAPI, _ = webrtc.NewAPI() } diff --git a/pkg/webrtc/api.go b/pkg/webrtc/api.go index 79cf6d3c..5551d65e 100644 --- a/pkg/webrtc/api.go +++ b/pkg/webrtc/api.go @@ -1,7 +1,9 @@ package webrtc import ( + "fmt" "net" + "slices" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/xnet" @@ -27,6 +29,69 @@ type Filters struct { UDPPorts []uint16 `yaml:"udp_ports"` } +func (f *Filters) Network(protocol string) string { + if f == nil || f.Networks == nil { + return protocol + } + v4 := slices.Contains(f.Networks, protocol+"4") + v6 := slices.Contains(f.Networks, protocol+"6") + if v4 && v6 { + return protocol + } else if v4 { + return protocol + "4" + } else if v6 { + return protocol + "6" + } + return "" +} + +func (f *Filters) NetIPs() (ips []net.IP) { + itfs, _ := net.Interfaces() + for _, itf := range itfs { + if itf.Flags&net.FlagUp == 0 { + continue + } + if !f.IncludeLoopback() && itf.Flags&net.FlagLoopback != 0 { + continue + } + if !f.InterfaceFilter(itf.Name) { + continue + } + + addrs, _ := itf.Addrs() + for _, addr := range addrs { + ip := parseNetAddr(addr) + if ip == nil || !f.IPFilter(ip) { + continue + } + ips = append(ips, ip) + } + } + return +} + +func parseNetAddr(addr net.Addr) net.IP { + switch addr := addr.(type) { + case *net.IPNet: + return addr.IP + case *net.IPAddr: + return addr.IP + } + return nil +} + +func (f *Filters) IncludeLoopback() bool { + return f != nil && f.Loopback +} + +func (f *Filters) InterfaceFilter(name string) bool { + return f == nil || f.Interfaces == nil || slices.Contains(f.Interfaces, name) +} + +func (f *Filters) IPFilter(ip net.IP) bool { + return f == nil || f.IPs == nil || core.Contains(f.IPs, ip.String()) +} + func NewServerAPI(network, address string, filters *Filters) (*webrtc.API, error) { // for debug logs add to env: `PION_LOG_DEBUG=all` m := &webrtc.MediaEngine{} @@ -99,48 +164,17 @@ func NewServerAPI(network, address string, filters *Filters) (*webrtc.API, error _ = s.SetEphemeralUDPPortRange(filters.UDPPorts[0], filters.UDPPorts[1]) } - //if len(hosts) != 0 { - // // support only: host, srflx - // if candidateType, err := webrtc.NewICECandidateType(hosts[0]); err == nil { - // s.SetNAT1To1IPs(hosts[1:], candidateType) - // } else { - // s.SetNAT1To1IPs(hosts, 0) // 0 = host - // } - //} - + // If you don't specify an address, this won't cause an error. + // Connections can still be established using random UDP addresses. if address != "" { + // Both newMux functions respect filters and do not raise an error + // if the port cannot be listened on. if network == "" || network == "tcp" { - if ln, err := net.Listen("tcp", address); err == nil { - tcpMux := webrtc.NewICETCPMux(nil, ln, 8) - s.SetICETCPMux(tcpMux) - } + tcpMux := newTCPMux(address, filters) + s.SetICETCPMux(tcpMux) } - if network == "" || network == "udp" { - // UDPMuxDefault should not listening on unspecified address, use NewMultiUDPMuxFromPort instead - var udpMux ice.UDPMux - if port := xnet.ParseUnspecifiedPort(address); port != 0 { - var networks []ice.NetworkType - for _, ntype := range networkTypes { - networks = append(networks, ice.NetworkType(ntype)) - } - - var err error - if udpMux, err = ice.NewMultiUDPMuxFromPort( - port, - ice.UDPMuxFromPortWithInterfaceFilter(interfaceFilter), - ice.UDPMuxFromPortWithIPFilter(ipFilter), - ice.UDPMuxFromPortWithNetworks(networks...), - ); err != nil { - return nil, err - } - } else { - ln, err := net.ListenPacket("udp", address) - if err != nil { - return nil, err - } - udpMux = ice.NewUDPMuxDefault(ice.UDPMuxParams{UDPConn: ln}) - } + udpMux := newUDPMux(address, filters) s.SetICEUDPMux(udpMux) } } @@ -152,6 +186,57 @@ func NewServerAPI(network, address string, filters *Filters) (*webrtc.API, error ), nil } +// OnNewListener temporary ugly solution for log +var OnNewListener = func(ln any) {} + +func newTCPMux(address string, filters *Filters) ice.TCPMux { + networkTCP := filters.Network("tcp") // tcp or tcp4 or tcp6 + if ln, _ := net.Listen(networkTCP, address); ln != nil { + OnNewListener(ln) + return webrtc.NewICETCPMux(nil, ln, 8) + } + return nil +} + +func newUDPMux(address string, filters *Filters) ice.UDPMux { + host, port, err := net.SplitHostPort(address) + if err != nil { + return nil + } + + // UDPMux should not listening on unspecified address. + // So we will create a listener on all available interfaces. + // We can't use ice.NewMultiUDPMuxFromPort, because it sometimes crashes with an error: + // listen udp [***]:8555: bind: cannot assign requested address + var addrs []string + if host == "" { + for _, ip := range filters.NetIPs() { + addrs = append(addrs, fmt.Sprintf("%s:%s", ip, port)) + } + } else { + addrs = []string{address} + } + + networkUDP := filters.Network("udp") // udp or udp4 or udp6 + + var muxes []ice.UDPMux + for _, addr := range addrs { + if ln, _ := net.ListenPacket(networkUDP, addr); ln != nil { + OnNewListener(ln) + mux := ice.NewUDPMuxDefault(ice.UDPMuxParams{UDPConn: ln}) + muxes = append(muxes, mux) + } + } + + switch len(muxes) { + case 0: + return nil + case 1: + return muxes[0] + } + return ice.NewMultiUDPMuxDefault(muxes...) +} + func RegisterDefaultCodecs(m *webrtc.MediaEngine) error { for _, codec := range []webrtc.RTPCodecParameters{ { diff --git a/pkg/xiaomi/cs2/conn.go b/pkg/xiaomi/cs2/conn.go index 45c9e704..198b3beb 100644 --- a/pkg/xiaomi/cs2/conn.go +++ b/pkg/xiaomi/cs2/conn.go @@ -1,6 +1,7 @@ package cs2 import ( + "bufio" "encoding/binary" "fmt" "io" @@ -10,33 +11,27 @@ import ( "time" ) -func Dial(host string) (*Conn, error) { - conn, err := net.ListenUDP("udp", nil) +func Dial(host, transport string) (*Conn, error) { + conn, err := handshake(host, transport) if err != nil { return nil, err } + _, isTCP := conn.(*tcpConn) + c := &Conn{ - conn: conn, - addr: &net.UDPAddr{IP: net.ParseIP(host), Port: 32108}, + conn: conn, + isTCP: isTCP, + rawCh0: make(chan []byte, 10), + rawCh2: make(chan []byte, 100), } - - if err = c.handshake(); err != nil { - _ = conn.Close() - return nil, err - } - - c.rawCh0 = make(chan []byte, 10) - c.rawCh2 = make(chan []byte, 100) - go c.worker() - return c, nil } type Conn struct { - conn *net.UDPConn - addr *net.UDPAddr + conn net.Conn + isTCP bool err error seqCh0 uint16 @@ -53,30 +48,58 @@ const ( magicDrw = 0xD1 msgLanSearch = 0x30 msgPunchPkt = 0x41 - msgP2PRdy = 0x42 + msgP2PRdyUDP = 0x42 + msgP2PRdyTCP = 0x43 msgDrw = 0xD0 msgDrwAck = 0xD1 - msgAlive = 0xE0 + msgPing = 0xE0 + msgPong = 0xE1 + msgClose = 0xF1 ) -func (c *Conn) handshake() error { - _ = c.SetDeadline(time.Now().Add(5 * time.Second)) - - buf, err := c.WriteAndWait([]byte{magic, msgLanSearch, 0, 0}, msgPunchPkt) +func handshake(host, transport string) (net.Conn, error) { + conn, err := newUDPConn(host, 32108) if err != nil { - return fmt.Errorf("%s: read punch: %w", "cs2", err) + return nil, err } - _, err = c.WriteAndWait(buf, msgP2PRdy) + _ = conn.SetDeadline(time.Now().Add(5 * time.Second)) + + req := []byte{magic, msgLanSearch, 0, 0} + res, err := conn.(*udpConn).WriteUntil(req, func(res []byte) bool { + return res[1] == msgPunchPkt + }) if err != nil { - return fmt.Errorf("%s: read ready: %w", "cs2", err) + _ = conn.Close() + return nil, err } - _ = c.Write([]byte{magic, msgAlive, 0, 0}) + var msgUDP, msgTCP byte - _ = c.SetDeadline(time.Time{}) + if transport == "" || transport == "udp" { + msgUDP = msgP2PRdyUDP + } + if transport == "" || transport == "tcp" { + msgTCP = msgP2PRdyTCP + } - return nil + res, err = conn.(*udpConn).WriteUntil(res, func(res []byte) bool { + return res[1] == msgUDP || res[1] == msgTCP + }) + if err != nil { + _ = conn.Close() + return nil, err + } + + _ = conn.SetDeadline(time.Time{}) + + if res[1] == msgTCP { + _ = conn.Close() + //host := fmt.Sprintf("%d.%d.%d.%d:%d", b[31], b[30], b[29], b[28], uint16(b[27])<<8|uint16(b[26])) + return newTCPConn(conn.RemoteAddr().String()) + } + + return conn, nil } func (c *Conn) worker() { @@ -85,38 +108,41 @@ func (c *Conn) worker() { close(c.rawCh2) }() - chAck := make([]uint16, 4) + chAck := make([]uint16, 4) // only for UDP buf := make([]byte, 1200) var ch2WaitSize int var ch2WaitData []byte + var keepaliveTS time.Time for { - n, addr, err := c.conn.ReadFromUDP(buf) + n, err := c.conn.Read(buf) if err != nil { c.err = fmt.Errorf("%s: %w", "cs2", err) return } - if string(addr.IP) != string(c.addr.IP) || n < 8 || buf[0] != magic { - continue // skip messages from another IP - } - - //log.Printf("<- %x", buf[:n]) - switch buf[1] { case msgDrw: ch := buf[5] - seqHI := buf[6] - seqLO := buf[7] - if chAck[ch] != uint16(seqHI)<<8|uint16(seqLO) { - continue - } - chAck[ch]++ + if c.isTCP { + // For TCP we should using ping/pong. + if now := time.Now(); now.After(keepaliveTS) { + _, _ = c.conn.Write([]byte{magic, msgPing, 0, 0}) + keepaliveTS = now.Add(5 * time.Second) + } + } else { + // For UDP we should using ack. + seqHI := buf[6] + seqLO := buf[7] - ack := []byte{magic, msgDrwAck, 0, 6, magicDrw, ch, 0, 1, seqHI, seqLO} - if _, err = c.conn.WriteToUDP(ack, c.addr); err != nil { - return + if chAck[ch] != uint16(seqHI)<<8|uint16(seqLO) { + continue + } + chAck[ch]++ + + ack := []byte{magic, msgDrwAck, 0, 6, magicDrw, ch, 0, 1, seqHI, seqLO} + _, _ = c.conn.Write(ack) } switch ch { @@ -152,9 +178,12 @@ func (c *Conn) worker() { continue } - case msgP2PRdy: // skip it + case msgPing: + _, _ = c.conn.Write([]byte{magic, msgPong, 0, 0}) continue - case msgDrwAck: + case msgPong, msgP2PRdyUDP, msgP2PRdyTCP, msgClose: + continue // skip it + case msgDrwAck: // only for UDP if c.cmdAck != nil { c.cmdAck() } @@ -165,42 +194,15 @@ func (c *Conn) worker() { } } -func (c *Conn) Write(req []byte) error { - //log.Printf("-> %x", req) - _, err := c.conn.WriteToUDP(req, c.addr) - return err -} - -func (c *Conn) WriteAndWait(req []byte, waitMsg uint8) ([]byte, error) { - var t *time.Timer - t = time.AfterFunc(1, func() { - if err := c.Write(req); err == nil && t != nil { - t.Reset(time.Second) - } - }) - defer t.Stop() - - buf := make([]byte, 1200) - - for { - n, addr, err := c.conn.ReadFromUDP(buf) - if err != nil { - return nil, err - } - - if string(addr.IP) != string(c.addr.IP) || n < 16 { - continue // skip messages from another IP - } - - if buf[0] == magic && buf[1] == waitMsg { - c.addr.Port = addr.Port - return buf[:n], nil - } +func (c *Conn) Protocol() string { + if c.isTCP { + return "cs2+tcp" } + return "cs2+udp" } func (c *Conn) RemoteAddr() net.Addr { - return c.addr + return c.conn.RemoteAddr() } func (c *Conn) SetDeadline(t time.Time) error { @@ -232,6 +234,14 @@ func (c *Conn) WriteCommand(cmd uint16, data []byte) error { c.cmdMu.Lock() defer c.cmdMu.Unlock() + req := marshalCmd(0, c.seqCh0, uint32(cmd), data) + c.seqCh0++ + + if c.isTCP { + _, err := c.conn.Write(req) + return err + } + var repeat atomic.Int32 repeat.Store(5) @@ -243,11 +253,8 @@ func (c *Conn) WriteCommand(cmd uint16, data []byte) error { timeout.Reset(1) } - req := marshalCmd(0, c.seqCh0, uint32(cmd), data) - c.seqCh0++ - for { - if err := c.Write(req); err != nil { + if _, err := c.conn.Write(req); err != nil { return err } <-timeout.C @@ -285,7 +292,8 @@ func (c *Conn) WritePacket(data []byte) error { binary.BigEndian.PutUint32(req[8:], n) copy(req[offset:], data) - return c.Write(req) + _, err := c.conn.Write(req) + return err } func marshalCmd(channel byte, seq uint16, cmd uint32, payload []byte) []byte { @@ -313,3 +321,112 @@ func marshalCmd(channel byte, seq uint16, cmd uint32, payload []byte) []byte { return req } + +func newUDPConn(host string, port int) (net.Conn, error) { + // We using raw net.UDPConn, because RemoteAddr should be changed during handshake. + conn, err := net.ListenUDP("udp", nil) + if err != nil { + return nil, err + } + + addr, err := net.ResolveUDPAddr("udp", host) + if err != nil { + addr = &net.UDPAddr{IP: net.ParseIP(host), Port: port} + } + + return &udpConn{UDPConn: conn, addr: addr}, nil +} + +type udpConn struct { + *net.UDPConn + addr *net.UDPAddr +} + +func (c *udpConn) Read(p []byte) (n int, err error) { + var addr *net.UDPAddr + for { + n, addr, err = c.UDPConn.ReadFromUDP(p) + if err != nil { + return 0, err + } + + if string(addr.IP) == string(c.addr.IP) || n >= 8 { + return + } + } +} + +func (c *udpConn) Write(req []byte) (n int, err error) { + //log.Printf("-> %x", req) + return c.UDPConn.WriteToUDP(req, c.addr) +} + +func (c *udpConn) RemoteAddr() net.Addr { + return c.addr +} + +func (c *udpConn) WriteUntil(req []byte, ok func(res []byte) bool) ([]byte, error) { + var t *time.Timer + t = time.AfterFunc(1, func() { + if _, err := c.Write(req); err == nil && t != nil { + t.Reset(time.Second) + } + }) + defer t.Stop() + + buf := make([]byte, 1200) + + for { + n, addr, err := c.UDPConn.ReadFromUDP(buf) + if err != nil { + return nil, err + } + + if string(addr.IP) != string(c.addr.IP) || n < 16 { + continue // skip messages from another IP + } + + if ok(buf[:n]) { + c.addr.Port = addr.Port + return buf[:n], nil + } + } +} + +func newTCPConn(addr string) (net.Conn, error) { + conn, err := net.DialTimeout("tcp", addr, 3*time.Second) + if err != nil { + return nil, err + } + return &tcpConn{conn.(*net.TCPConn), bufio.NewReader(conn)}, nil +} + +type tcpConn struct { + *net.TCPConn + rd *bufio.Reader +} + +func (c *tcpConn) Read(p []byte) (n int, err error) { + tmp := make([]byte, 8) + if _, err = io.ReadFull(c.rd, tmp); err != nil { + return + } + n = int(binary.BigEndian.Uint16(tmp)) + if len(p) < n { + return 0, fmt.Errorf("tcp: buffer too small") + } + _, err = io.ReadFull(c.rd, p[:n]) + //log.Printf("<- %x%x", tmp, p[:n]) + return +} + +func (c *tcpConn) Write(req []byte) (n int, err error) { + n = len(req) + buf := make([]byte, 8+n) + binary.BigEndian.PutUint16(buf, uint16(n)) + buf[2] = 0x68 + copy(buf[8:], req) + //log.Printf("-> %x", buf) + _, err = c.TCPConn.Write(buf) + return +} diff --git a/pkg/xiaomi/miss/client.go b/pkg/xiaomi/miss/client.go index a335968d..f7157ccf 100644 --- a/pkg/xiaomi/miss/client.go +++ b/pkg/xiaomi/miss/client.go @@ -33,7 +33,7 @@ func Dial(rawURL string) (*Client, error) { switch s := query.Get("vendor"); s { case "cs2": - c.conn, err = cs2.Dial(u.Host) + c.conn, err = cs2.Dial(u.Host, query.Get("transport")) case "tutk": c.conn, err = tutk.Dial(u.Host, query.Get("uid")) default: @@ -63,6 +63,7 @@ const ( ) type Conn interface { + Protocol() string ReadCommand() (cmd uint16, data []byte, err error) WriteCommand(cmd uint16, data []byte) error ReadPacket() ([]byte, error) @@ -77,6 +78,10 @@ type Client struct { key []byte } +func (c *Client) Protocol() string { + return c.conn.Protocol() +} + func (c *Client) RemoteAddr() net.Addr { return c.conn.RemoteAddr() } @@ -89,16 +94,6 @@ func (c *Client) Close() error { return c.conn.Close() } -func (c *Client) Protocol() string { - switch c.conn.(type) { - case *cs2.Conn: - return "cs2+udp" - case *tutk.Conn: - return "tutk+udp" - } - return "" -} - const ( cmdAuthReq = 0x100 cmdAuthRes = 0x101 @@ -276,6 +271,24 @@ type Packet struct { Payload []byte } +func (p *Packet) SampleRate() uint32 { + // flag: 1 0011 000 - sample rate 16000 + // flag: 100 00 01 0000 000 - sample rate 8000 + v := (p.Flags >> 3) & 0b1111 + if v != 0 { + return 16000 + } + return 8000 +} + +//func (p *Packet) AudioUnknown1() byte { +// return byte((p.Flags >> 7) & 0b11) +//} +// +//func (p *Packet) AudioUnknown2() byte { +// return byte((p.Flags >> 9) & 0b11) +//} + func GenerateKey() ([]byte, []byte, error) { public, private, err := box.GenerateKey(rand.Reader) if err != nil { diff --git a/pkg/xiaomi/producer.go b/pkg/xiaomi/producer.go index 09ba7360..dcd419b8 100644 --- a/pkg/xiaomi/producer.go +++ b/pkg/xiaomi/producer.go @@ -110,7 +110,7 @@ func probe(client *miss.Client, channel, quality, audio uint8) ([]*core.Media, e } case miss.CodecPCMA: if acodec == nil { - acodec = &core.Codec{Name: core.CodecPCMA, ClockRate: 8000} + acodec = &core.Codec{Name: core.CodecPCMA, ClockRate: pkt.SampleRate()} } case miss.CodecOPUS: if acodec == nil { diff --git a/pkg/xiaomi/tutk/conn.go b/pkg/xiaomi/tutk/conn.go index bce4a795..f3005224 100644 --- a/pkg/xiaomi/tutk/conn.go +++ b/pkg/xiaomi/tutk/conn.go @@ -249,6 +249,10 @@ func (c *Conn) WriteAndWait(req []byte, ok func(req, res []byte) bool) ([]byte, } } +func (c *Conn) Protocol() string { + return "tutk+udp" +} + func (c *Conn) RemoteAddr() net.Addr { return c.addr } diff --git a/website/index.html b/website/index.html index 443c925c..e339eb44 100644 --- a/website/index.html +++ b/website/index.html @@ -1,189 +1,28 @@
- -
+github.com/AlexxIT/go2rtc
-
\ No newline at end of file
+