Compare commits

..

169 Commits

Author SHA1 Message Date
Alexey Khit f60b55b6fa Update version to 1.1.2 2023-02-09 07:48:28 +03:00
Alexey Khit c42413866d Remove RTSP wrong channel ID from logs 2023-02-09 07:48:11 +03:00
Alexey Khit b137eb66d0 Fix more sizes for RTSP MJPEG #83 2023-02-09 07:18:58 +03:00
Alexey Khit 6a40039645 Fix MJPEG processing for wallpanel project #248 2023-02-09 07:18:58 +03:00
Alexey Khit 2e4b28d871 Fix RTSP auth for RtspServer project #244 2023-02-09 07:18:58 +03:00
Alexey Khit 58146b7e7e Fix H265 processing for RtspServer project #244 2023-02-09 07:18:58 +03:00
Alexey Khit 23db40220b Fix H264 processing for RtspServer project #244 2023-02-09 07:18:58 +03:00
Alex X 557aac185d Merge pull request #220 from skrashevich/macos-lipo
Generate universal macOS binary on release
2023-02-09 07:18:06 +03:00
Alexey Khit 9ed4d4cedb Fix parsing SDP from Reolink Doorbell 2023-02-07 20:02:23 +03:00
Alexey Khit b05cbdf3d3 Make GetProfileLevelID func more smarter 2023-02-06 20:53:55 +03:00
Alexey Khit 497594f53f Fix buggy SDP parsing 2023-02-06 11:46:00 +03:00
Alexey Khit 73cdb39335 Add camera experience section to readme 2023-02-06 09:32:01 +03:00
Sergey Krashevich a388002b12 Merge branch 'master' into macos-lipo 2023-02-04 20:40:46 +03:00
Alexey Khit 6d1c0a2459 Fix SDP parsing from cheap Chinese cameras 2023-02-04 10:01:35 +03:00
Alexey Khit da3137b6f0 Add User-Agent to RTSP Describe #235 2023-02-03 14:11:30 +03:00
Alexey Khit d21ce3d27d Jump over wrong packets from RTSP 2023-02-03 12:54:49 +03:00
Alexey Khit 8cee4179f2 Fix another buggy Chinese cameras 2023-02-03 12:54:49 +03:00
Alexey Khit 1153ee3652 Fix support WebRTC for Chromecast 1 2023-02-03 11:41:51 +03:00
Alexey Khit 3240301f27 Fix autofullscreen with MP4 for iPhones 2023-02-03 11:41:43 +03:00
Alexey Khit 2a20251dbd Fix autoplay after background 2023-02-03 11:41:37 +03:00
Alexey Khit 5a2d7de56b Add projects using go2rtc section to readme 2023-02-03 11:38:26 +03:00
Alexey Khit 38ea8b56b8 Update version to 1.1.1 2023-02-01 17:57:00 +03:00
Alexey Khit 08c2174e94 Fix default_query bug #227 2023-02-01 10:40:29 +03:00
Alexey Khit b48f1c1a0b Update default_query param name in API response 2023-02-01 10:39:54 +03:00
Alexey Khit cf58a6f952 Update readme about Hass integration 2023-01-31 19:47:54 +03:00
Alexey Khit 350e677838 Update readme 2023-01-31 11:30:36 +03:00
Alexey Khit 7b3505f4f4 Update version to 1.1.0 2023-01-31 10:32:28 +03:00
Alexey Khit 98af8c3dbf Update links page 2023-01-31 08:56:49 +03:00
Alexey Khit 762edf157a Add default_query setting for RTSP server 2023-01-31 07:35:50 +03:00
Alexey Khit 4a633cd9b5 Move stream useful links to separate page 2023-01-30 23:02:06 +03:00
Alexey Khit f4d2c801f0 Add redirect for Safari from MP4 to HLS 2023-01-30 22:00:07 +03:00
Alexey Khit fb4b609914 Add support output as HLS (TS+fMP4) 2023-01-30 21:22:12 +03:00
Alexey Khit 56633229ed Fix AAC support for old MP4 consumer 2023-01-30 21:21:17 +03:00
Alexey Khit 2d49cfd4b6 Code refactoring 2023-01-30 19:15:32 +03:00
Alexey Khit 0f934be9b6 Add MimeCodecs to mp4 Muxer 2023-01-30 19:15:12 +03:00
Alexey Khit c1d6adc189 Move ParseQuery from rtsp to mp4 module 2023-01-30 19:13:35 +03:00
Alexey Khit 500b8720d5 Fix bug with no stream from some Dahua cameras 2023-01-29 18:55:37 +03:00
Sergey Krashevich b7391f58a5 Update release.yml 2023-01-28 03:21:03 +03:00
Alexey Khit bef8e6454d Update RTSP Server response with all tracks by default 2023-01-27 20:43:56 +03:00
Alexey Khit 5243aca8e9 Remove Title field from Media object 2023-01-27 19:30:48 +03:00
Alexey Khit 69dd4d26ec Add support OPUS, MP3, PCMU, PCMA for MP4 2023-01-27 17:11:44 +03:00
Alexey Khit e93d89ec96 Add mp3 preset for ffmpeg 2023-01-27 17:10:41 +03:00
Alexey Khit ec56227900 Add codecs filter to stream.mp4 2023-01-27 17:05:45 +03:00
Alexey Khit decd3af941 Add OR to RTSP Server codecs filter 2023-01-27 17:05:01 +03:00
Alexey Khit e8e43f9d68 Fix MSE in Safari 2023-01-27 12:39:51 +03:00
Alexey Khit a1fec1c6f6 Add support OPUS audio for MSE/MP4 2023-01-27 12:37:02 +03:00
Alexey Khit 073acdfec9 Code refactoring 2023-01-27 12:27:19 +03:00
Alexey Khit d05ab79f88 Total rewrite mov/mp4 encoder 2023-01-26 22:29:12 +03:00
Alexey Khit e295bc4eaf Fix RTSP AAC sound from some Reolink cameras 2023-01-26 22:02:02 +03:00
Alexey Khit 2f436bba4e Fix RTSP URL parse bug #208 2023-01-26 09:09:48 +03:00
Alexey Khit 0e28b0c797 Fix API base_path support #205 2023-01-25 16:40:06 +03:00
Alexey Khit 3acea1ed5a Update version to 1.0.1 2023-01-24 22:29:15 +03:00
Alexey Khit 3fb8d9af66 Disable release autobuild 2023-01-24 22:29:04 +03:00
Alexey Khit 9bbaf41d54 Second fix for Chinese buggy cameras 2023-01-24 21:38:58 +03:00
Alexey Khit c43530fbd3 Fix mp4f consumer 2023-01-24 21:05:51 +03:00
Alexey Khit 15777a3d94 Fix Chinese buggy cameras 2023-01-24 21:05:35 +03:00
Alexey Khit 6e61ac6d2f Fix HTTP-FLV for Reolink cameras 2023-01-24 17:48:31 +03:00
Alexey Khit 6d7d5f53d8 Update websocket disconnect log message 2023-01-24 17:48:08 +03:00
Alexey Khit d2bca8d461 Update processing HTTP-FLV without video or audio 2023-01-24 17:47:26 +03:00
Alexey Khit 94b089d1e3 Fix bug in URL for D-Link cameras 2023-01-23 21:14:52 +03:00
Alexey Khit b3d16c9fcc Update TOC in readme 2023-01-23 15:37:06 +03:00
Alexey Khit f0def68482 Update readme 2023-01-20 17:45:35 +03:00
Alexey Khit 9ddbb326b4 Update version to 1.0.0 2023-01-20 17:07:43 +03:00
Alexey Khit a2e58d928e Fix timezone in logs 2023-01-20 13:45:01 +03:00
Alexey Khit 3c48fb8bea Simplify Dockerfile 2023-01-20 11:23:28 +03:00
Alexey Khit 4b0cbb5a73 Add support basic auth for API 2023-01-20 10:54:26 +03:00
Alexey Khit e28b49ea86 Ignore errors for RTCP packets 2023-01-20 10:26:57 +03:00
Alexey Khit 5c17d8fcb6 Add support AAC audio for HTTP-FLV 2023-01-19 21:44:15 +03:00
Alexey Khit e040fb591f Disable CGO for git releases 2023-01-18 15:07:42 +03:00
Alexey Khit 140014f2a6 Fix info for WS/MP4 2023-01-18 15:04:06 +03:00
Alexey Khit 23f72d111e Add Teardown handler for RTSP server (untested) 2023-01-18 12:21:54 +03:00
Alexey Khit f9d5ab9d0a Fix RTSP server SDP for some clients 2023-01-18 11:45:39 +03:00
Alexey Khit 8628c48db8 Add no-cache for all GET API requests 2023-01-18 10:01:00 +03:00
Alexey Khit 6e49d51c33 Update GET config API when config file not set 2023-01-18 10:00:20 +03:00
Alexey Khit 6a61b5234e Fix HTTP-FLV support for Reolink cameras 2023-01-18 09:36:32 +03:00
Alexey Khit 7a0091777d Fix relative config path #171 2023-01-16 11:00:04 +03:00
Alexey Khit d23d2a7eff Fix release binaries for mac 2023-01-16 00:40:02 +03:00
Alexey Khit cecbe4166c Update version to 0.1-rc.9 2023-01-16 00:06:55 +03:00
Alexey Khit dcb457235c Rewrite stream info API 2023-01-15 23:51:20 +03:00
Alexey Khit bc4e032830 Update readme 2023-01-15 11:13:38 +03:00
Alexey Khit 8218cda149 Add version, config_path to web UI and fix RTSP link 2023-01-15 09:57:15 +03:00
Alexey Khit d1e56feeb6 Update full path to config file 2023-01-15 09:55:32 +03:00
Alexey Khit 463d05dfd3 Update readme 2023-01-15 00:28:48 +03:00
Alexey Khit a1a73f7b45 Rewrite WS+MP4 format to keyframes stream 2023-01-15 00:12:26 +03:00
Alexey Khit 39662e10af Fix errors in JS player 2023-01-15 00:11:31 +03:00
Alexey Khit 1c830d6e60 Code refactoring 2023-01-14 22:49:12 +03:00
Alex X 2039aa60b3 Merge pull request #170 from skrashevich/config-api-patch-method
PATH api/config method for merge configuration
2023-01-14 21:57:34 +03:00
Sergey Krashevich b7016e798f Update config.go 2023-01-14 21:27:23 +03:00
Alexey Khit 0b291f5185 Support multiple configs and config in raw yaml form 2023-01-14 21:12:17 +03:00
Alexey Khit 395304654a Code refactoring 2023-01-14 19:15:13 +03:00
Alexey Khit e472397705 Add general info API 2023-01-14 18:00:43 +03:00
Alexey Khit 7c1f48e0ad Support empty default environment value 2023-01-14 17:25:05 +03:00
Alexey Khit f4346a104f Add support env variables in config file #143 2023-01-14 17:19:51 +03:00
Alexey Khit 030972b436 Auto build binaries on release #158 2023-01-14 14:14:23 +03:00
Alexey Khit efddefa123 Add web config editor #153 2023-01-14 13:47:34 +03:00
Alexey Khit 3c1bdd0dab Fix WebRTC candidate type 2023-01-14 09:45:03 +03:00
Alexey Khit 7e7e15d7c8 Update readme 2023-01-14 09:22:22 +03:00
Alex X a1a9f77535 Merge pull request #167 from felipecrs/master
Match docs with new webrtc udp fixed port
2023-01-14 09:10:46 +03:00
Alexey Khit a06462729d Code refactoring 2023-01-14 09:04:54 +03:00
Alex X 331c5bbcad Merge pull request #166 from tsightler/udp-candidate-fix
Fix invalid tcpType for UDP candidate
2023-01-14 08:59:25 +03:00
Felipe Santos 58a76efc8a Match docs with new webrtc udp fixed port 2023-01-13 23:15:04 -03:00
tsightler 5e0f010885 Update helper.go 2023-01-13 18:18:39 -05:00
Alexey Khit 4ae733aa11 Update version to 0.1-rc.8 2023-01-13 22:39:24 +03:00
Alexey Khit 27d8b33b62 Fix concurrency in ivideon 2023-01-13 21:52:29 +03:00
Alexey Khit ff8b0fbb9c Set default 8555 port for WebRTC (UDP+TCP) 2023-01-13 21:51:48 +03:00
Alexey Khit c6ad7ac39f Add single UDP port for WebRTC Server 2023-01-13 21:51:48 +03:00
Alexey Khit 7a3adf17be Fix mp4f consumer (unused) 2023-01-13 21:51:24 +03:00
Alexey Khit 94f6c07b28 Fix mjpeg client network connection 2023-01-13 18:03:54 +03:00
Alexey Khit 7b326d4753 Fix simultaneous stream reconnect and start 2023-01-13 18:03:17 +03:00
Alexey Khit 5407a3bc4b Fix multiple requests from different consumers 2023-01-13 18:02:03 +03:00
Alexey Khit 6b24421722 Fix unblocking exec error 2023-01-13 18:01:01 +03:00
Alexey Khit d12775a2d7 Fix unblocking exec waiter 2023-01-13 18:00:48 +03:00
Alexey Khit 6151593c08 Fix ws lock on write and close 2023-01-13 17:28:01 +03:00
Alexey Khit dba0989c54 Fix empty streams json on stream lock 2023-01-13 13:37:36 +03:00
Alexey Khit ba0c7d911d Fix ffmpeg link to same stream 2023-01-13 13:36:43 +03:00
Alexey Khit 09fefca712 Remove backchannel codec from add consumer error 2023-01-13 13:35:58 +03:00
Alexey Khit b3f177e2ec Handle closed state for ws connection 2023-01-13 13:34:41 +03:00
Alexey Khit 228abb8fbe Change logs msg from WRN to DBG for fail on add consumer 2023-01-13 13:33:55 +03:00
Alexey Khit eee70c07b7 Fix closer for ivideon source 2023-01-13 13:32:48 +03:00
Alexey Khit d92b0f29af Fix states handle for RTSP 2023-01-13 13:32:09 +03:00
Alexey Khit fca6c87b2c Fix RTSP tracks list in info json 2023-01-13 13:31:22 +03:00
Alexey Khit 0601091772 Fix closer for RTSP server #163 2023-01-13 13:30:41 +03:00
Alexey Khit 89eb653d67 Update version to 0.1-rc.7 2023-01-08 23:18:52 +03:00
Alexey Khit 0e49ffdfff Fix GetMedias for producer in reconnect state 2023-01-08 21:42:13 +03:00
Alexey Khit bd2fc1252d Update last error for reconnect stream 2023-01-08 21:36:28 +03:00
Alexey Khit 78ac88448c Fix close problem ivideon client 2023-01-08 21:35:45 +03:00
Alexey Khit 4cd9757e53 Fix status info in JS player 2023-01-08 21:05:50 +03:00
Alexey Khit f9cb6fd670 Fix wrong RTSP H264 profile for some cameras 2023-01-08 21:05:17 +03:00
Alexey Khit 57fa6a5530 Add support for simultaneous requests from different consumers 2023-01-08 20:31:00 +03:00
Alexey Khit 6906b56524 Fix double start for RTSP source 2023-01-08 20:01:38 +03:00
Alexey Khit c9b0806c84 Add producer url to logs 2023-01-08 20:00:48 +03:00
Alexey Khit a9d1e64f88 Fix STUN candidate in IPv6 format 2023-01-08 15:45:11 +03:00
Alex X 9e9f07f3f7 Merge pull request #150 from skrashevich/dockerfile-crossbuild
Speedup container building using Golang cross-building
2023-01-06 14:06:50 +03:00
Sergey Krashevich b51aabd3d9 Update Dockerfile 2023-01-06 11:52:09 +03:00
Alexey Khit 368562c540 Update version to 0.1-rc.6 2023-01-02 20:53:04 +03:00
Alexey Khit 6d6e7010b4 Rewrite JS player for better integration 2023-01-02 16:33:00 +03:00
Alexey Khit 4157a53dd8 Response with error on codec negotiation 2023-01-02 16:32:08 +03:00
Alexey Khit bdf5654c01 Change WS default buffer 2023-01-02 16:31:11 +03:00
Alexey Khit 66f729aa0e Send WS response on MJPEG or MP4 stream starts 2023-01-02 16:30:54 +03:00
Alexey Khit 96d1ef2d2c Adds about RTSPtoWebRTC STUN server to readme 2022-12-27 15:59:08 +03:00
Alexey Khit 9739f7f416 Add auto create new stream for async webrtc 2022-12-25 11:17:14 +03:00
Alexey Khit 654fa32b3a Fix packet size for MSE 2022-12-25 11:09:19 +03:00
Alexey Khit db2263c7fe Fix stream page for raw urls in src 2022-12-25 08:55:58 +03:00
Alexey Khit e6c36f1cf7 Rename Hardware Dockerfile 2022-12-19 12:16:38 +03:00
Alexey Khit 110f90cb34 Disable JS stream background by default 2022-12-18 22:39:52 +03:00
Alexey Khit aca3bab238 Fix Firefox WebRTC support 2022-12-18 22:38:44 +03:00
Alexey Khit 4df44645d7 Fix lags for Intel HW transcoding 2022-12-18 21:33:38 +03:00
Alexey Khit 097fdfbbb8 Rename HW engine for Raspberry 2022-12-18 10:25:04 +03:00
Alexey Khit dc21a04da7 Fix freezing with VAAPI HW 2022-12-18 10:24:30 +03:00
Alexey Khit db255b476a Add hardware acceleration support to FFmpeg 2022-12-18 01:02:12 +03:00
Alexey Khit 464ea417ef Add docker image with Hardware drivers 2022-12-18 01:00:39 +03:00
Alexey Khit c1fac66329 Refactoring CI 2022-12-17 23:40:56 +03:00
Alex X a6057a2eca Merge pull request #72 from felipecrs/refactor-docker
Refactor docker image and ci
2022-12-17 23:07:55 +03:00
Alexey Khit 7c69ba13b0 Fix RTP H264 with two SEI in packet 2022-12-17 22:59:06 +03:00
Alexey Khit 2b8bfe8bd9 Add support width and height params for FFmpeg 2022-12-09 22:07:57 +03:00
Alexey Khit 0bd54da456 Increase compression level for 7zip 2022-12-09 00:25:38 +03:00
Alexey Khit 9f6af1c9e4 Update connection method in JS player so it can be extended 2022-12-09 00:24:29 +03:00
Alexey Khit c9dd0e37e4 Fix RTSP JPEG processing 2022-12-09 00:22:25 +03:00
Felipe Santos 562872beb8 Add jq 2022-12-06 10:38:18 -03:00
Felipe Santos 46a278c067 Add curl and exit on error run.sh 2022-12-06 10:38:18 -03:00
Felipe Santos 270fc7c1b6 Allow to run container without mounting /config 2022-12-06 10:38:18 -03:00
Felipe Santos 6feb635522 Delete config.yaml not used anymore 2022-12-06 10:38:18 -03:00
Felipe Santos 6f48131e4d Remove armv6 which was never supported 2022-12-06 10:38:18 -03:00
Felipe Santos f120db71a3 Add all platforms 2022-12-06 10:38:18 -03:00
Felipe Santos 72823af9d0 Refactor docker workflow 2022-12-06 10:38:18 -03:00
Felipe Santos 15d9d4ebf4 Use official python as base and add tini 2022-12-06 10:38:18 -03:00
Felipe Santos b09bbd79c4 Fix VERSION 2022-12-06 10:38:18 -03:00
Felipe Santos 1830273f02 Refactor docker image 2022-12-06 10:38:18 -03:00
Alexey Khit 07f3972794 Update readme 2022-12-06 16:17:28 +03:00
96 changed files with 5204 additions and 1391 deletions
-59
View File
@@ -1,59 +0,0 @@
# https://github.com/home-assistant/builder
name: 'Builder'
on:
push:
tags: [ 'v*' ]
workflow_dispatch:
jobs:
hassio:
name: Hassio Addon
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v3
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Branch name
run: |
VERSION="${GITHUB_REF#refs/tags/v}"
echo "REPO=alexxit/go2rtc" >> $GITHUB_ENV
echo "TAG=${VERSION}" >> $GITHUB_ENV
echo "IMAGE=alexxit/go2rtc:${VERSION}" >> $GITHUB_ENV
- name: Build amd64
uses: home-assistant/builder@master
with:
args: --amd64 --target build/hassio --version $TAG-amd64 --no-latest --docker-hub-check
- name: Build i386
uses: home-assistant/builder@master
with:
args: --i386 --target build/hassio --version $TAG-i386 --no-latest --docker-hub-check
- name: Build aarch64
uses: home-assistant/builder@master
with:
args: --aarch64 --target build/hassio --version $TAG-aarch64 --no-latest --docker-hub-check
- name: Build armv7
uses: home-assistant/builder@master
with:
args: --armv7 --target build/hassio --version $TAG-armv7 --no-latest --docker-hub-check
- name: Docker manifest
run: |
# thanks to https://github.com/aler9/rtsp-simple-server/blob/main/Makefile
docker manifest create "${IMAGE}" \
"${IMAGE}-amd64" "${IMAGE}-i386" "${IMAGE}-aarch64" "${IMAGE}-armv7"
docker manifest push "${IMAGE}"
docker manifest create "${REPO}:latest" \
"${IMAGE}-amd64" "${IMAGE}-i386" "${IMAGE}-aarch64" "${IMAGE}-armv7"
docker manifest push "${REPO}:latest"
+75
View File
@@ -0,0 +1,75 @@
name: docker
on:
workflow_dispatch:
push:
branches:
- 'master'
tags:
- 'v*'
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ github.repository }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}},enable=false
type=match,pattern=v(.*),group=1
- name: Docker meta Hardware
id: meta-hw
uses: docker/metadata-action@v4
with:
images: ${{ github.repository }}
flavor: |
suffix=-hardware
latest=false
tags: |
type=ref,event=branch
type=semver,pattern={{version}},enable=false
type=match,pattern=v(.*),group=1
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
platforms: |
linux/amd64
linux/386
linux/arm/v7
linux/arm64/v8
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Build and push Hardware
uses: docker/build-push-action@v3
with:
context: .
file: hardware.Dockerfile
platforms: linux/amd64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta-hw.outputs.tags }}
labels: ${{ steps.meta-hw.outputs.labels }}
+99
View File
@@ -0,0 +1,99 @@
name: release
on:
workflow_dispatch:
# push:
# tags:
# - 'v*'
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Generate changelog
run: |
echo -e "$(git log $(git describe --tags --abbrev=0)..HEAD --oneline | awk '{print "- "$0}')" > CHANGELOG.md
- name: install lipo
run: |
curl -L -o /tmp/lipo https://github.com/konoui/lipo/releases/latest/download/lipo_Linux_amd64
chmod +x /tmp/lipo
mv /tmp/lipo /usr/local/bin
- name: Build Go binaries
run: |
#!/bin/bash
export CGO_ENABLED=0
mkdir -p artifacts
export GOOS=windows
export GOARCH=amd64
export FILENAME=artifacts/go2rtc_win64.zip
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc.exe
export GOOS=windows
export GOARCH=386
export FILENAME=artifacts/go2rtc_win32.zip
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc.exe
export GOOS=windows
export GOARCH=arm64
export FILENAME=artifacts/go2rtc_win_arm64.zip
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc.exe
export GOOS=linux
export GOARCH=amd64
export FILENAME=artifacts/go2rtc_linux_amd64
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
export GOOS=linux
export GOARCH=386
export FILENAME=artifacts/go2rtc_linux_i386
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
export GOOS=linux
export GOARCH=arm64
export FILENAME=artifacts/go2rtc_linux_arm64
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
export GOOS=linux
export GOARCH=arm
export GOARM=7
export FILENAME=artifacts/go2rtc_linux_arm
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
export GOOS=linux
export GOARCH=mipsle
export FILENAME=artifacts/go2rtc_linux_mipsel
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
export GOOS=darwin
export GOARCH=amd64
go build -ldflags "-s -w" -trimpath -o go2rtc.amd64
export GOOS=darwin
export GOARCH=arm64
go build -ldflags "-s -w" -trimpath -o go2rtc.arm64
export FILENAME=artifacts/go2rtc_mac_universal.zip
lipo -output go2rtc -create go2rtc.arm64 go2rtc.amd64 && 7z a -mx9 -sdel "$FILENAME" go2rtc
parallel --jobs $(nproc) "upx {}" ::: artifacts/go2rtc_linux_*
- name: Setup tmate session
uses: mxschmitt/action-tmate@v3
if: ${{ failure() }}
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Create GitHub release
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: artifacts/*
generate_release_notes: true
name: Release ${{ env.RELEASE_VERSION }}
body_path: CHANGELOG.md
draft: false
prerelease: false
+61
View File
@@ -0,0 +1,61 @@
# syntax=docker/dockerfile:labs
# 0. Prepare images
ARG PYTHON_VERSION="3.11"
ARG GO_VERSION="1.19"
ARG NGROK_VERSION="3"
FROM python:${PYTHON_VERSION}-alpine AS base
FROM ngrok/ngrok:${NGROK_VERSION}-alpine AS ngrok
# 1. Build go2rtc binary
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS build
ARG TARGETPLATFORM
ARG TARGETOS
ARG TARGETARCH
ENV GOOS=${TARGETOS}
ENV GOARCH=${TARGETARCH}
WORKDIR /build
# Cache dependencies
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/root/.cache/go-build go mod download
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
# 2. Collect all files
FROM scratch AS rootfs
COPY --from=build /build/go2rtc /usr/local/bin/
COPY --from=ngrok /bin/ngrok /usr/local/bin/
# 3. Final image
FROM base
# Install ffmpeg, tini (for signal handling),
# and other common tools for the echo source.
RUN apk add --no-cache tini ffmpeg bash curl jq
# Hardware Acceleration for Intel CPU (+50MB)
ARG TARGETARCH
RUN if [ "${TARGETARCH}" = "amd64" ]; then apk add --no-cache libva-intel-driver intel-media-driver; fi
# Hardware: AMD and NVidia VAAPI (not sure about this)
# RUN libva-glx mesa-va-gallium
# Hardware: AMD and NVidia VDPAU (not sure about this)
# RUN libva-vdpau-driver mesa-vdpau-gallium (+150MB total)
COPY --from=rootfs / /
ENTRYPOINT ["/sbin/tini", "--"]
VOLUME /config
WORKDIR /config
CMD ["go2rtc", "-config", "/config/go2rtc.yaml"]
+226 -91
View File
@@ -6,10 +6,10 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
- zero-dependency and zero-config [small app](#go2rtc-binary) for all OS (Windows, macOS, Linux, ARM)
- zero-delay for many supported protocols (lowest possible streaming latency)
- streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [MJPEG](#source-ffmpeg), [HLS/HTTP](#source-ffmpeg), [USB Cameras](#source-ffmpeg-device) and [other sources](#module-streams)
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4) or [MJPEG](#module-mjpeg)
- streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [HTTP](#source-http) (FLV/MJPEG/JPEG), [FFmpeg](#source-ffmpeg), [USB Cameras](#source-ffmpeg-device) and [other sources](#module-streams)
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4), [HLS](#module-hls) or [MJPEG](#module-mjpeg)
- first project in the World with support streaming from [HomeKit Cameras](#source-homekit)
- first project in the World with support H265 for WebRTC in browser ([read more](https://github.com/AlexxIT/Blog/issues/5))
- first project in the World with 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)
- multi-source 2-way [codecs negotiation](#codecs-negotiation)
- mixing tracks from different sources to single stream
@@ -27,17 +27,52 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
- [MediaSoup](https://mediasoup.org/) framework routing idea
- HomeKit Accessory Protocol from [@brutella](https://github.com/brutella/hap)
---
* [Fast start](#fast-start)
* [go2rtc: Binary](#go2rtc-binary)
* [go2rtc: Docker](#go2rtc-docker)
* [go2rtc: Home Assistant Add-on](#go2rtc-home-assistant-add-on)
* [go2rtc: Home Assistant Integration](#go2rtc-home-assistant-integration)
* [Configuration](#configuration)
* [Module: Streams](#module-streams)
* [Source: RTSP](#source-rtsp)
* [Source: RTMP](#source-rtmp)
* [Source: HTTP](#source-http)
* [Source: FFmpeg](#source-ffmpeg)
* [Source: FFmpeg Device](#source-ffmpeg-device)
* [Source: Exec](#source-exec)
* [Source: Echo](#source-echo)
* [Source: HomeKit](#source-homekit)
* [Source: Ivideon](#source-ivideon)
* [Source: Hass](#source-hass)
* [Module: API](#module-api)
* [Module: RTSP](#module-rtsp)
* [Module: WebRTC](#module-webrtc)
* [Module: Ngrok](#module-ngrok)
* [Module: Hass](#module-hass)
* [Module: MP4](#module-mp4)
* [Module: HLS](#module-hls)
* [Module: MJPEG](#module-mjpeg)
* [Module: Log](#module-log)
* [Security](#security)
* [Codecs filters](#codecs-filters)
* [Codecs madness](#codecs-madness)
* [Codecs negotiation](#codecs-negotiation)
* [Projects using go2rtc](#projects-using-go2rtc)
* [Camera experience](#cameras-experience)
* [TIPS](#tips)
* [FAQ](#faq)
## Fast start
1. Download [binary](#go2rtc-binary) or use [Docker](#go2rtc-docker) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on)
1. Download [binary](#go2rtc-binary) or use [Docker](#go2rtc-docker) or Home Assistant [Add-on](#go2rtc-home-assistant-add-on) or [Integration](#go2rtc-home-assistant-integration)
2. Open web interface: `http://localhost:1984/`
**Optionally:**
- add your [streams](#module-streams) to [config](#configuration) file
- setup [external access](#module-webrtc) to webrtc
- setup [external access](#module-ngrok) to web interface
- install [ffmpeg](#source-ffmpeg) for transcoding
**Developers:**
@@ -54,12 +89,16 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2
- `go2rtc_linux_i386` - Linux 32-bit
- `go2rtc_linux_arm64` - Linux ARM 64-bit (ex. Raspberry 64-bit OS)
- `go2rtc_linux_arm` - Linux ARM 32-bit (ex. Raspberry 32-bit OS)
- `go2rtc_linux_mipsel` - Linux on MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3))
- `go2rtc_mac_amd64` - Mac with Intel
- `go2rtc_mac_arm64` - Mac with M1
- `go2rtc_linux_mipsel` - Linux MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3))
- `go2rtc_mac_amd64.zip` - Mac Intel 64-bit
- `go2rtc_mac_arm64.zip` - Mac ARM 64-bit
Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
### go2rtc: Docker
Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container is the same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg), [Ngrok](#module-ngrok) and [Python](#source-echo).
### go2rtc: Home Assistant Add-on
[![](https://my.home-assistant.io/badges/supervisor_addon.svg)](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a889bffc_go2rtc&repository_url=https%3A%2F%2Fgithub.com%2FAlexxIT%2Fhassio-addons)
@@ -69,29 +108,19 @@ Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
- go2rtc > Install > Start
2. Setup [Integration](#module-hass)
### go2rtc: Docker
### go2rtc: Home Assistant Integration
Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from the Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg), [Ngrok](#module-ngrok) and [Python](#source-echo).
```yaml
services:
go2rtc:
image: alexxit/go2rtc
network_mode: host
restart: always
volumes:
- "~/go2rtc.yaml:/config/go2rtc.yaml"
```
[WebRTC Camera](https://github.com/AlexxIT/WebRTC) custom component can be used on any [Home Assistant installation](https://www.home-assistant.io/installation/), including [HassWP](https://github.com/AlexxIT/HassWP) on Windows. It can automatically download and use the latest version of go2rtc. Or it can connect to an existing version of go2rtc. Addon installation in this case is optional.
## Configuration
Create file `go2rtc.yaml` next to the app.
- by default go2rtc will search `go2rtc.yaml` in the current work dirrectory
- `api` server will start on default **1984 port** (TCP)
- `rtsp` server will start on default **8554 port** (TCP)
- `webrtc` will use port **8555** (TCP/UDP) for connections
- `ffmpeg` will use default transcoding options
- by default, you need to config only your `streams` links
- `api` server will start on default **1984 port**
- `rtsp` server will start on default **8554 port**
- `webrtc` will use random UDP port for each connection
- `ffmpeg` will use default transcoding options (you may install it [manually](https://ffmpeg.org/))
Configuration options and a complete list of settings can be found in [the wiki](https://github.com/AlexxIT/go2rtc/wiki/Configuration).
Available modules:
@@ -100,6 +129,7 @@ Available modules:
- [rtsp](#module-rtsp) - RTSP Server (important for FFmpeg support)
- [webrtc](#module-webrtc) - WebRTC Server
- [mp4](#module-mp4) - MSE, MP4 stream and MP4 shapshot Server
- [hls](#module-hls) - HLS TS or fMP4 stream Server
- [mjpeg](#module-mjpeg) - MJPEG Server
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
- [ngrok](#module-ngrok) - Ngrok integration (external access for private network)
@@ -215,7 +245,7 @@ But you can override them via YAML config. You can also add your own formats to
```yaml
ffmpeg:
bin: ffmpeg # path to ffmpeg binary
bin: ffmpeg # path to ffmpeg binary
h264: "-codec:v libx264 -g:v 30 -preset:v superfast -tune:v zerolatency -profile:v main -level:v 4.1"
mycodec: "-any args that support ffmpeg..."
```
@@ -223,8 +253,11 @@ ffmpeg:
- You can use `video` and `audio` params multiple times (ex. `#video=copy#audio=copy#audio=pcmu`)
- You can use go2rtc stream name as ffmpeg input (ex. `ffmpeg:camera1#video=h264`)
- You can use `rotate` params with `90`, `180`, `270` or `-90` values, important with transcoding (ex. `#video=h264#rotate=90`)
- You can use `width` and/or `height` params, important with transcoding (ex. `#video=h264#width=1280`)
- You can use `raw` param for any additional FFmpeg arguments (ex. `#raw=-vf transpose=1`).
Read more about encoding [hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration).
#### Source: FFmpeg Device
You can get video from any USB-camera or Webcam as RTSP or WebRTC stream. This is part of FFmpeg integration.
@@ -329,7 +362,34 @@ More cameras, like [Tuya](https://www.home-assistant.io/integrations/tuya/), [ON
The HTTP API is the main part for interacting with the application. Default address: `http://127.0.0.1:1984/`.
- you can use WebRTC only when HTTP API enabled
go2rtc has its own JS video player (`video-rtc.js`) with:
- support technologies:
- WebRTC over UDP or TCP
- MSE or MP4 or MJPEG over WebSocket
- automatic selection best technology according on:
- codecs inside your stream
- current browser capabilities
- current network configuration
- automatic stop stream while browser or page not active
- automatic stop stream while player not inside page viewport
- automatic reconnection
Technology selection based on priorities:
1. Video and Audio better than just Video
2. H265 better than H264
3. WebRTC better than MSE, than MP4, than MJPEG
go2rtc has simple HTML page (`stream.html`) with support params in URL:
- multiple streams on page `src=camera1&src=camera2...`
- stream technology autoselection `mode=webrtc,mse,mp4,mjpeg`
- stream technology comparison `src=camera1&mode=webrtc&mode=mse&mode=mp4`
- player width setting in pixels `width=320px` or percents `width=50%`
**Module config**
- you can disable HTTP API with `listen: ""` and use, for example, only RTSP client/server protocol
- 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
@@ -337,77 +397,85 @@ The HTTP API is the main part for interacting with the application. Default addr
```yaml
api:
listen: ":1984" # HTTP API port ("" - disabled)
base_path: "/rtc" # API prefix for serve on suburl (/api => /rtc/api)
static_dir: "www" # folder for static files (custom web interface)
origin: "*" # allow CORS requests (only * supported)
listen: ":1984" # default ":1984", HTTP API port ("" - disabled)
username: "admin" # default "", Basic auth for WebUI
password: "pass" # default "", Basic auth for WebUI
base_path: "/rtc" # default "", API prefix for serve on suburl (/api => /rtc/api)
static_dir: "www" # default "", folder for static files (custom web interface)
origin: "*" # default "", allow CORS requests (only * supported)
```
**PS. go2rtc** doesn't provide HTTPS or password protection. Use [Nginx](https://nginx.org/) or [Ngrok](#module-ngrok) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on) for this tasks.
**PS:**
**PS2.** You can access microphone (for 2-way audio) only with HTTPS ([read more](https://stackoverflow.com/questions/52759992/how-to-access-camera-and-microphone-in-chrome-without-https)).
- go2rtc doesn't provide HTTPS. Use [Nginx](https://nginx.org/) or [Ngrok](#module-ngrok) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on) for this tasks
- you can access microphone (for 2-way audio) only with HTTPS ([read more](https://stackoverflow.com/questions/52759992/how-to-access-camera-and-microphone-in-chrome-without-https))
- MJPEG over WebSocket plays better than native MJPEG because Chrome [bug](https://bugs.chromium.org/p/chromium/issues/detail?id=527446)
- MP4 over WebSocket was created only for Apple iOS because it doesn't support MSE and native MP4
### Module: RTSP
You can get any stream as RTSP-stream: `rtsp://192.168.1.123:8554/{stream_name}`
- you can omit the codec filters, so one first video and one first audio will be selected
- you can set `?video=copy` or just `?video`, so only one first video without audio will be selected
- you can set multiple video or audio, so all of them will be selected
- you can enable external password protection for your RTSP streams
Password protection always disabled for localhost calls (ex. FFmpeg or Hass on same server)
You can enable external password protection for your RTSP streams. Password protection always disabled for localhost calls (ex. FFmpeg or Hass on same server).
```yaml
rtsp:
listen: ":8554" # RTSP Server TCP port, default - 8554
username: admin # optional, default - disabled
password: pass # optional, default - disabled
listen: ":8554" # RTSP Server TCP port, default - 8554
username: "admin" # optional, default - disabled
password: "pass" # optional, default - disabled
default_query: "video&audio" # optional, default codecs filters
```
By default go2rtc provide RTSP-stream with only one first video and only one first audio. You can change it with the `default_query` setting:
- `default_query: "mp4"` - MP4 compatible codecs (H264, H265, AAC)
- `default_query: "video=all&audio=all"` - all tracks from all source (not all players can handle this)
- `default_query: "video=h264,h265"` - only one video track (H264 or H265)
- `default_query: "video&audio=all"` - only one first any video and all audio as separate tracks
Read more about [codecs filters](#codecs-filters).
### Module: WebRTC
WebRTC usually works without problems in the local network. But external access may require additional settings. It depends on what type of Internet do you have.
- by default, WebRTC use two random UDP ports for each connection (video and audio)
- you can enable one additional TCP port for all connections and use it for external access
- by default, WebRTC uses both TCP and UDP on port 8555 for connections
- you can use this port for external access
- you can change the port in YAML config:
```yaml
webrtc:
listen: ":8555" # address of your local server and port (TCP/UDP)
```
**Static public IP**
- add some TCP port to YAML config (ex. 8555)
- forward this port on your router (you can use same 8555 port or any other)
- forward the port 8555 on your router (you can use same 8555 port or any other as external port)
- add your external IP-address and external port to YAML config
```yaml
webrtc:
listen: ":8555" # address of your local server (TCP)
candidates:
- 216.58.210.174:8555 # if you have static public IP-address
```
**Dynamic public IP**
- add some TCP port to YAML config (ex. 8555)
- forward this port on your router (you can use same 8555 port or any other)
- forward the port 8555 on your router (you can use same 8555 port or any other as the external port)
- add `stun` word and external port to YAML config
- go2rtc automatically detects your external address with STUN-server
```yaml
webrtc:
listen: ":8555" # address of your local server (TCP)
candidates:
- stun:8555 # if you have dynamic public IP-address
```
**Private IP**
- add some TCP port to YAML config (ex. 8555)
- setup integration with [Ngrok service](#module-ngrok)
```yaml
webrtc:
listen: ":8555" # address of your local server (TCP)
ngrok:
command: ...
```
@@ -485,23 +553,29 @@ tunnels:
### Module: Hass
If you install **go2rtc** as [Hass Add-on](#go2rtc-home-assistant-add-on) - you need to use localhost IP-address. In other cases you need to use IP-address of server with **go2rtc** application.
The best and easiest way to use go2rtc inside the Home Assistant is to install the custom integration [WebRTC Camera](#go2rtc-home-assistant-integration) and custom lovelace card.
#### From go2rtc to Hass
But go2rtc is also compatible and can be used with [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) built-in integration.
Add any supported [stream source](#module-streams) as [Generic Camera](https://www.home-assistant.io/integrations/generic/) and view stream with built-in [Stream](https://www.home-assistant.io/integrations/stream/) integration. Technology `HLS`, supported codecs: `H264`, poor latency.
You have several options on how to add a camera to Home Assistant:
1. Add your stream to [go2rtc config](#configuration)
2. 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`
1. Camera RTSP source => [Generic Camera](https://www.home-assistant.io/integrations/generic/)
2. Camera [any source](#module-streams) => [go2rtc config](#configuration) => [Generic Camera](https://www.home-assistant.io/integrations/generic/)
- Install any [go2rtc](#fast-start)
- Add your stream to [go2rtc config](#configuration)
- 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)
#### From Hass to go2rtc
You have several options on how to watch the stream from the cameras in Home Assistant:
View almost any Hass camera using `WebRTC` technology, supported codecs `H264`/`PCMU`/`PCMA`/`OPUS`, best latency.
When the stream starts - the camera `entity_id` will be added to go2rtc "on the fly". You don't need to add cameras manually to [go2rtc config](#configuration). Some cameras (like [Nest](https://www.home-assistant.io/integrations/nest/)) have a dynamic link to the stream, it will be updated each time a stream is started from the Hass interface.
1. Hass > Settings > Integrations > Add Integration > [RTSPtoWebRTC](https://my.home-assistant.io/redirect/config_flow_start/?domain=rtsp_to_webrtc) > `http://127.0.0.1:1984/`
2. Use Picture Entity or Picture Glance lovelace card
1. `Camera Entity` => `Picture Entity Card` => Technology `HLS`, codecs: `H264/H265/AAC`, poor latency.
2. `Camera Entity` => [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) => `Picture Entity Card` => Technology `WebRTC`, codecs: `H264/PCMU/PCMA/OPUS`, best latency.
- Install any [go2rtc](#fast-start)
- Hass > Settings > Integrations > Add Integration > [RTSPtoWebRTC](https://my.home-assistant.io/redirect/config_flow_start/?domain=rtsp_to_webrtc) > `http://127.0.0.1:1984/`
- RTSPtoWebRTC > Configure > STUN server: `stun.l.google.com:19302`
- Use Picture Entity or Picture Glance lovelace card
3. `Camera Entity` or `Camera URL` => [WebRTC Camera](https://github.com/AlexxIT/WebRTC) => Technology: `WebRTC/MSE/MP4/MJPEG`, codecs: `H264/H265/AAC/PCMU/PCMA/OPUS`, best latency, best compatibility.
- Install and add [WebRTC Camera](https://github.com/AlexxIT/WebRTC) custom integration
- Use WebRTC Camera custom lovelace card
You can add camera `entity_id` to [go2rtc config](#configuration) if you need transcoding:
@@ -517,8 +591,28 @@ PS. Default Home Assistant lovelace cards don't support 2-way audio. You can use
Provides several features:
1. MSE stream (fMP4 over WebSocket)
2. Camera snapshots in MP4 format (single frame), can be sent to [Telegram](https://www.telegram.org/)
3. MP4 "file stream" - bad format for streaming because of high latency, doesn't work in Safari
2. Camera snapshots in MP4 format (single frame), can be sent to [Telegram](https://github.com/AlexxIT/go2rtc/wiki/Snapshot-to-Telegram)
3. MP4 "file stream" - bad format for streaming because of high start delay. This format doesn't work in all Safari browsers, but go2rtc will automatically redirect it to HLS/fMP4 it this case.
API examples:
- MP4 stream: `http://192.168.1.123:1984/api/stream.mp4?src=camera1`
- MP4 snapshot: `http://192.168.1.123:1984/api/frame.mp4?src=camera1`
Read more about [codecs filters](#codecs-filters).
### Module: HLS
[HLS](https://en.wikipedia.org/wiki/HTTP_Live_Streaming) is the worst technology for real-time streaming. It can only be useful on devices that do not support more modern technology, like [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4).
The go2rtc implementation differs from the standards and may not work with all players.
API examples:
- HLS/TS stream: `http://192.168.1.123:1984/api/stream.m3u8?src=camera1` (H264)
- HLS/fMP4 stream: `http://192.168.1.123:1984/api/stream.m3u8?src=camera1&mp4` (H264, H265, AAC)
Read more about [codecs filters](#codecs-filters).
### Module: MJPEG
@@ -562,7 +656,7 @@ log:
## Security
By default `go2rtc` start Web interface on port `1984` and RTSP on port `8554`. Both ports are accessible from your local network. So anyone on your local network can watch video from your cameras without authorization. The same rule applies to the Home Assistant Add-on.
By default `go2rtc` starts the Web interface on port `1984` and RTSP on port `8554`, as well as use port `8555` for WebRTC connections. The three ports are accessible from your local network. So anyone on your local network can watch video from your cameras without authorization. The same rule applies to the Home Assistant Add-on.
This is not a problem if you trust your local network as much as I do. But you can change this behaviour with a `go2rtc.yaml` config:
@@ -574,7 +668,7 @@ rtsp:
listen: "127.0.0.1:8554" # localhost
webrtc:
listen: ":8555" # external TCP port
listen: ":8555" # external TCP/UDP port
```
- local access to RTSP is not a problem for [FFmpeg](#source-ffmpeg) integration, because it runs locally on your server
@@ -584,23 +678,44 @@ webrtc:
If you need Web interface protection without Home Assistant Add-on - you need to use reverse proxy, like [Nginx](https://nginx.org/), [Caddy](https://caddyserver.com/), [Ngrok](https://ngrok.com/), etc.
PS. Additionally WebRTC opens a lot of random UDP ports for transmit encrypted media. They work without problems on the local network. And sometimes work for external access, even if you haven't opened ports on your router. But for stable external WebRTC access, you need to configure the TCP port.
PS. Additionally WebRTC will try to use the 8555 UDP port for transmit encrypted media. It works without problems on the local network. And sometimes also works for external access, even if you haven't opened this port on your router ([read more](https://en.wikipedia.org/wiki/UDP_hole_punching)). But for stable external WebRTC access, you need to open the 8555 port on your router for both TCP and UDP.
## Codecs filters
go2rtc can automatically detect which codecs your device supports for [WebRTC](#module-webrtc) and [MSE](#module-mp4) technologies.
But it cannot be done for [RTSP](#module-rtsp), [stream.mp4](#module-mp4), [HLS](#module-hls) technologies. You can manually add a codec filter when you create a link to a stream. The filters work the same for all three technologies. Filters do not create a new codec. They only select the suitable codec from existing sources. You can add new codecs to the stream using the [FFmpeg transcoding](#source-ffmpeg).
Without filters:
- RTSP will provide only the first video and only the first audio
- MP4 will include only compatible codecs (H264, H265, AAC)
- HLS will output in the legacy TS format (H264 without audio)
Some examples:
- `rtsp://192.168.1.123:8554/camera1?mp4` - useful for recording as MP4 files (e.g. Hass or Frigate)
- `rtsp://192.168.1.123:8554/camera1?video=h264,h265&audio=aac` - full version of the filter above
- `rtsp://192.168.1.123:8554/camera1?video=h264&audio=aac&audio=opus` - H264 video codec and two separate audio tracks
- `rtsp://192.168.1.123:8554/camera1?video&audio=all` - any video codec and all audio codecs as separate tracks
- `http://192.168.1.123:1984/api/stream.m3u8?src=camera1&mp4` - HLS stream with MP4 compatible codecs (HLS/fMP4)
- `http://192.168.1.123:1984/api/stream.mp4?src=camera1&video=h264,h265&audio=aac,opus,mp3,pcma,pcmu` - MP4 file with non standard audio codecs, does not work in some players
## Codecs madness
`AVC/H.264` codec can be played almost anywhere. But `HEVC/H.265` has a lot of limitations in supporting with different devices and browsers. It's all about patents and money, you can't do anything about it.
`AVC/H.264` video can be played almost anywhere. But `HEVC/H.265` has a lot of limitations in supporting with different devices and browsers. It's all about patents and money, you can't do anything about it.
| Device | WebRTC | MSE | MP4 |
|---------------------|-------------|-------------|-------------|
| *latency* | best | medium | bad |
| Desktop Chrome 107+ | H264 | H264, H265* | H264, H265* |
| Desktop Edge | H264 | H264, H265* | H264, H265* |
| Desktop Safari | H264, H265* | H264, H265 | **no!** |
| Desktop Firefox | H264 | H264 | H264 |
| Android Chrome 107+ | H264 | H264, H265* | H264 |
| iPad Safari 13+ | H264, H265* | H264, H265 | **no!** |
| iPhone Safari 13+ | H264, H265* | **no!** | **no!** |
| masOS Hass App | no | no | no |
| Device | WebRTC | MSE | stream.mp4 |
|---------------------|-------------------------------|------------------------|-----------------------------------------|
| *latency* | best | medium | bad |
| Desktop Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, OPUS | H264, H265*, AAC, OPUS, PCMU, PCMA, MP3 |
| Desktop Edge | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, OPUS | H264, H265*, AAC, OPUS, PCMU, PCMA, MP3 |
| Desktop Safari | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC | **no!** |
| Desktop Firefox | H264, OPUS, PCMU, PCMA | H264, AAC, OPUS | H264, AAC, OPUS |
| Android Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, OPUS | H264, ?, AAC, OPUS, PCMU, PCMA, MP3 |
| iPad Safari 13+ | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC | **no!** |
| iPhone Safari 13+ | H264, H265*, OPUS, PCMU, PCMA | **no!** | **no!** |
| masOS Hass App | no | no | no |
- Chrome H265: [read this](https://chromestatus.com/feature/5186511939567616) and [read this](https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding)
- Edge H265: [read this](https://www.reddit.com/r/MicrosoftEdge/comments/v9iw8k/enable_hevc_support_in_edge/)
@@ -609,8 +724,9 @@ PS. Additionally WebRTC opens a lot of random UDP ports for transmit encrypted m
**Audio**
- WebRTC audio codecs: `PCMU/8000`, `PCMA/8000`, `OPUS/48000/2`
- MSE/MP4 audio codecs: `AAC`
- **WebRTC** audio codecs: `PCMU/8000`, `PCMA/8000`, `OPUS/48000/2`
- `OPUS` and `MP3` inside **MP4** is part of the standard, but some players do not support them anyway (especially Apple)
- `PCMU` and `PCMA` inside **MP4** isn't a standard, but some players support them, for example Chromium browsers
**Apple devices**
@@ -647,6 +763,21 @@ streams:
**PS.** You can select `PCMU` or `PCMA` codec in camera setting and don't use transcoding at all. Or you can select `AAC` codec for main stream and `PCMU` codec for second stream and add both RTSP to YAML config, this also will work fine.
## Projects using go2rtc
- [Frigate 12+](https://frigate.video/) - open source NVR built around real-time AI object detection
- [ring-mqtt](https://github.com/tsightler/ring-mqtt) - Ring devices to MQTT Bridge
- [EufyP2PStream](https://github.com/oischinger/eufyp2pstream) - A small project that provides a Video/Audio Stream from Eufy cameras that don't directly support RTSP
## Cameras experience
- [Dahua](https://www.dahuasecurity.com/) - reference implementation streaming protocols, a lot of settings, high stream quality, multiple streaming clients
- [Hikvision](https://www.hikvision.com/) - a lot of proprietary streaming technologies
- [Reolink](https://reolink.com/) - some models has awful unusable RTSP realisation and not best HTTP-FLV alternative (I recommend that you contact Reolink support for new firmware), few settings
- [Sonoff](https://sonoff.tech/) - very low stream quality, no settings, not best protocol implementation
- [TP-Link](https://www.tp-link.com/) - few streaming clients, packet loss?
- Chinese cheap noname cameras, Wyze Cams, Xiaomi cameras with hacks (usual has `/live/ch00_1` in RTSP URL) - awful but usable RTSP protocol realisation, low stream quality, few settings, packet loss?
## TIPS
**Using apps for low RTSP delay**
@@ -654,23 +785,27 @@ streams:
- `ffplay -fflags nobuffer -flags low_delay "rtsp://192.168.1.123:8554/camera1"`
- VLC > Preferences > Input / Codecs > Default Caching Level: Lowest Latency
**Snapshots to Telegram**
[read more](https://github.com/AlexxIT/go2rtc/wiki/Snapshot-to-Telegram)
## FAQ
**Q. What's the difference between go2rtc, WebRTC Camera and RTSPtoWebRTC?**
**go2rtc** is a new version of the server-side [WebRTC Camera](https://github.com/AlexxIT/WebRTC) integration, completely rewritten from scratch, with a number of fixes and a huge number of new features. It is compatible with native Home Assistant [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration. So you [can use](#module-hass) default lovelace Picture Entity or Picture Glance.
**Q. Why go2rtc is an addon and not an integration?**
**Q. Should I use go2rtc addon or WebRTC Camera integration?**
Because **go2rtc** is more than just viewing your stream online with WebRTC. You can use it all the time for your various tasks. But every time the Hass is rebooted - all integrations are also rebooted. So your streams may be interrupted if you use them in additional tasks.
**go2rtc** is more than just viewing your stream online with WebRTC/MSE/HLS/etc. You can use it all the time for your various tasks. But every time the Hass is rebooted - all integrations are also rebooted. So your streams may be interrupted if you use them in additional tasks.
When **go2rtc** is released, the **WebRTC Camera** integration will be updated. And you can decide whether to use the integration or the addon.
Basic users can use **WebRTC Camera** integration. Advanced users can use go2rtc addon or Frigate 12+ addon.
**Q. Which RTSP link should I use inside Hass?**
You can use direct link to your cameras there (as you always do). **go2rtc** support zero-config feature. You may leave `streams` config section empty. And your streams will be created on the fly on first start from Hass. And your cameras will have multiple connections. Some from Hass directly and one from **go2rtc**.
Also you can specify your streams in **go2rtc** [config file](#configuration) and use RTSP links to this addon. With additional features: multi-source [codecs negotiation](#codecs-negotiation) or FFmpeg [transcoding](#source-ffmpeg) for unsupported codecs. Or use them as source for Frigate. And your cameras will have one connection from **go2rtc**. And **go2rtc** will have multiple connection - some from Hass via RTSP protocol, some from your browser via WebRTC protocol.
Also you can specify your streams in **go2rtc** [config file](#configuration) and use RTSP links to this addon. With additional features: multi-source [codecs negotiation](#codecs-negotiation) or FFmpeg [transcoding](#source-ffmpeg) for unsupported codecs. Or use them as source for Frigate. And your cameras will have one connection from **go2rtc**. And **go2rtc** will have multiple connection - some from Hass via RTSP protocol, some from your browser via WebRTC/MSE/HLS protocols.
Use any config what you like.
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
set -euo pipefail
echo "Starting go2rtc..." >&2
readonly config_path="/config"
if [[ -x "${config_path}/go2rtc" ]]; then
readonly binary_path="${config_path}/go2rtc"
echo "Using go2rtc binary from '${binary_path}' instead of the embedded one" >&2
else
readonly binary_path="/usr/local/bin/go2rtc"
fi
# set cwd for go2rtc (for config file, Hass integration, etc)
cd "${config_path}" || echo "Could not change working directory to '${config_path}'" >&2
exec "${binary_path}"
-41
View File
@@ -1,41 +0,0 @@
ARG BUILD_FROM
FROM $BUILD_FROM as build
# 1. Build go2rtc
RUN apk add --no-cache git go
RUN git clone https://github.com/AlexxIT/go2rtc \
&& cd go2rtc \
&& CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
# 2. Download ngrok
ARG BUILD_ARCH
# https://github.com/home-assistant/docker-base/blob/master/alpine/Dockerfile
RUN if [ "${BUILD_ARCH}" = "aarch64" ]; then BUILD_ARCH="arm64"; \
elif [ "${BUILD_ARCH}" = "armv7" ]; then BUILD_ARCH="arm"; fi \
&& cd go2rtc \
&& curl $(curl -s "https://raw.githubusercontent.com/ngrok/docker-ngrok/main/releases.json" | jq -r ".${BUILD_ARCH}.url") -o ngrok.zip \
&& unzip ngrok
# https://devopscube.com/reduce-docker-image-size/
FROM $BUILD_FROM
# 3. Copy go2rtc and ngrok to release
COPY --from=build /go2rtc/go2rtc /usr/local/bin
COPY --from=build /go2rtc/ngrok /usr/local/bin
# 4. Install ffmpeg
# apk base OK: 22 MiB in 40 packages
# ffmpeg OK: 113 MiB in 110 packages
# python3 OK: 161 MiB in 114 packages
RUN apk add --no-cache ffmpeg python3
# 5. Copy run to release
COPY run.sh /
RUN chmod a+x /run.sh
CMD [ "/run.sh" ]
-6
View File
@@ -1,6 +0,0 @@
# https://github.com/home-assistant/builder/blob/master/builder.sh
name: go2rtc
description: Ultimate camera streaming application
url: https://github.com/AlexxIT/go2rtc
image: alexxit/go2rtc
arch: [ amd64, aarch64, i386, armv7 ]
-14
View File
@@ -1,14 +0,0 @@
#!/usr/bin/with-contenv bashio
set +e
# set cwd for go2rtc (for config file, Hass integration, etc)
cd /config
# add the feature to override go2rtc binary from Hass config folder
export PATH="/config:$PATH"
while true; do
go2rtc
sleep 5
done
+54 -34
View File
@@ -3,16 +3,21 @@ package api
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/rs/zerolog"
"net"
"net/http"
"os"
"strconv"
"strings"
"sync"
)
func Init() {
var cfg struct {
Mod struct {
Listen string `yaml:"listen"`
Username string `yaml:"username"`
Password string `yaml:"password"`
BasePath string `yaml:"base_path"`
StaticDir string `yaml:"static_dir"`
Origin string `yaml:"origin"`
@@ -35,7 +40,9 @@ func Init() {
initStatic(cfg.Mod.StaticDir)
initWS(cfg.Mod.Origin)
HandleFunc("api/streams", streamsHandler)
HandleFunc("api", apiHandler)
HandleFunc("api/config", configHandler)
HandleFunc("api/exit", exitHandler)
HandleFunc("api/ws", apiWS)
// ensure we can listen without errors
@@ -48,14 +55,18 @@ func Init() {
log.Info().Str("addr", cfg.Mod.Listen).Msg("[api] listen")
s := http.Server{}
s.Handler = http.DefaultServeMux
if log.Trace().Enabled() {
s.Handler = middlewareLog(s.Handler)
}
s.Handler = http.DefaultServeMux // 4th
if cfg.Mod.Origin == "*" {
s.Handler = middlewareCORS(s.Handler)
s.Handler = middlewareCORS(s.Handler) // 3rd
}
if cfg.Mod.Username != "" {
s.Handler = middlewareAuth(cfg.Mod.Username, cfg.Mod.Password, s.Handler) // 2nd
}
if log.Trace().Enabled() {
s.Handler = middlewareLog(s.Handler) // 1st
}
go func() {
@@ -83,7 +94,22 @@ var log zerolog.Logger
func middlewareLog(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Trace().Msgf("[api] %s %s", r.Method, r.URL)
log.Trace().Msgf("[api] %s %s %s", r.Method, r.URL, r.RemoteAddr)
next.ServeHTTP(w, r)
})
}
func middlewareAuth(username, password string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.RemoteAddr, "127.") && !strings.HasPrefix(r.RemoteAddr, "[::1]") {
user, pass, ok := r.BasicAuth()
if !ok || user != username || pass != password {
w.Header().Set("Www-Authenticate", `Basic realm="go2rtc"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
}
next.ServeHTTP(w, r)
})
}
@@ -96,31 +122,25 @@ func middlewareCORS(next http.Handler) http.Handler {
})
}
func streamsHandler(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
name := r.URL.Query().Get("name")
var mu sync.Mutex
if name == "" {
name = src
func apiHandler(w http.ResponseWriter, r *http.Request) {
mu.Lock()
app.Info["host"] = r.Host
mu.Unlock()
if err := json.NewEncoder(w).Encode(app.Info); err != nil {
log.Warn().Err(err).Caller().Send()
}
switch r.Method {
case "PUT":
streams.New(name, src)
return
case "DELETE":
streams.Delete(src)
return
}
var v interface{}
if src != "" {
v = streams.Get(src)
} else {
v = streams.All()
}
e := json.NewEncoder(w)
e.SetIndent("", " ")
_ = e.Encode(v)
}
func exitHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "", http.StatusBadRequest)
return
}
s := r.URL.Query().Get("code")
code, _ := strconv.Atoi(s)
os.Exit(code)
}
+102
View File
@@ -0,0 +1,102 @@
package api
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"gopkg.in/yaml.v3"
"io"
"net/http"
"os"
)
func configHandler(w http.ResponseWriter, r *http.Request) {
if app.ConfigPath == "" {
http.Error(w, "", http.StatusGone)
return
}
switch r.Method {
case "GET":
data, err := os.ReadFile(app.ConfigPath)
if err != nil {
http.Error(w, "", http.StatusNotFound)
return
}
if _, err = w.Write(data); err != nil {
log.Warn().Err(err).Caller().Send()
}
case "POST", "PATCH":
data, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if r.Method == "PATCH" {
// no need to validate after merge
data, err = mergeYAML(app.ConfigPath, data)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
} else {
// validate config
var tmp struct{}
if err = yaml.Unmarshal(data, &tmp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
if err = os.WriteFile(app.ConfigPath, data, 0644); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
func mergeYAML(file1 string, yaml2 []byte) ([]byte, error) {
// Read the contents of the first YAML file
data1, err := os.ReadFile(file1)
if err != nil {
return nil, err
}
// Unmarshal the first YAML file into a map
var config1 map[string]interface{}
if err = yaml.Unmarshal(data1, &config1); err != nil {
return nil, err
}
// Unmarshal the second YAML document into a map
var config2 map[string]interface{}
if err = yaml.Unmarshal(yaml2, &config2); err != nil {
return nil, err
}
// Merge the two maps
config1 = merge(config1, config2)
// Marshal the merged map into YAML
return yaml.Marshal(&config1)
}
func merge(dst, src map[string]interface{}) map[string]interface{} {
for k, v := range src {
if vv, ok := dst[k]; ok {
switch vv := vv.(type) {
case map[string]interface{}:
v := v.(map[string]interface{})
dst[k] = merge(vv, v)
case []interface{}:
v := v.([]interface{})
dst[k] = v
default:
dst[k] = v
}
} else {
dst[k] = v
}
}
return dst
}
+24 -6
View File
@@ -6,6 +6,7 @@ import (
"net/url"
"strings"
"sync"
"time"
)
// Message - struct for data exchange in Web API
@@ -25,7 +26,7 @@ var wsHandlers = make(map[string]WSHandler)
func initWS(origin string) {
wsUp = &websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 512000,
WriteBufferSize: 2028,
}
switch origin {
@@ -68,6 +69,8 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
tr := &Transport{Request: r}
tr.OnWrite(func(msg interface{}) {
_ = ws.SetWriteDeadline(time.Now().Add(time.Second * 5))
if data, ok := msg.([]byte); ok {
_ = ws.WriteMessage(websocket.BinaryMessage, data)
} else {
@@ -78,7 +81,9 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
for {
msg := new(Message)
if err = ws.ReadJSON(msg); err != nil {
log.Trace().Err(err).Caller().Send()
if !websocket.IsCloseError(err, websocket.CloseNoStatusReceived) {
log.Trace().Err(err).Caller().Send()
}
_ = ws.Close()
break
}
@@ -101,7 +106,9 @@ type Transport struct {
Request *http.Request
Consumer interface{} // TODO: rewrite
mx sync.Mutex
closed bool
mx sync.Mutex
wrmx sync.Mutex
onChange func()
onWrite func(msg interface{})
@@ -118,21 +125,32 @@ func (t *Transport) OnWrite(f func(msg interface{})) {
}
func (t *Transport) Write(msg interface{}) {
t.mx.Lock()
t.wrmx.Lock()
t.onWrite(msg)
t.mx.Unlock()
t.wrmx.Unlock()
}
func (t *Transport) Close() {
t.mx.Lock()
for _, f := range t.onClose {
f()
}
t.closed = true
t.mx.Unlock()
}
func (t *Transport) OnChange(f func()) {
t.mx.Lock()
t.onChange = f
t.mx.Unlock()
}
func (t *Transport) OnClose(f func()) {
t.onClose = append(t.onClose, f)
t.mx.Lock()
if t.closed {
f()
} else {
t.onClose = append(t.onClose, f)
}
t.mx.Unlock()
}
+60 -20
View File
@@ -2,46 +2,76 @@ package app
import (
"flag"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"time"
)
var Version = "0.1-rc.5"
var Version = "1.1.2"
var UserAgent = "go2rtc/" + Version
func Init() {
config := flag.String(
"config",
"go2rtc.yaml",
"Path to go2rtc configuration file",
)
var ConfigPath string
var Info = map[string]interface{}{
"version": Version,
}
func Init() {
var confs Config
flag.Var(&confs, "config", "go2rtc config (path to file or raw text), support multiple")
flag.Parse()
data, _ = os.ReadFile(*config)
if confs == nil {
confs = []string{"go2rtc.yaml"}
}
for _, conf := range confs {
if conf[0] != '{' {
// config as file
if ConfigPath == "" {
ConfigPath = conf
}
data, _ := os.ReadFile(conf)
if data == nil {
continue
}
data = []byte(shell.ReplaceEnvVars(string(data)))
configs = append(configs, data)
} else {
// config as raw YAML
configs = append(configs, []byte(conf))
}
}
if ConfigPath != "" {
if !filepath.IsAbs(ConfigPath) {
if cwd, err := os.Getwd(); err == nil {
ConfigPath = filepath.Join(cwd, ConfigPath)
}
}
Info["config_path"] = ConfigPath
}
var cfg struct {
Mod map[string]string `yaml:"log"`
}
if data != nil {
if err := yaml.Unmarshal(data, &cfg); err != nil {
println("ERROR: " + err.Error())
}
}
LoadConfig(&cfg)
log.Logger = NewLogger(cfg.Mod["format"], cfg.Mod["level"])
modules = cfg.Mod
log.Info().Msgf("go2rtc version %s %s/%s", Version, runtime.GOOS, runtime.GOARCH)
path, _ := os.Getwd()
log.Debug().Str("cwd", path).Send()
}
func NewLogger(format string, level string) zerolog.Logger {
@@ -54,7 +84,7 @@ func NewLogger(format string, level string) zerolog.Logger {
}
}
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
zerolog.TimeFieldFormat = time.RFC3339Nano
lvl, err := zerolog.ParseLevel(level)
if err != nil || lvl == zerolog.NoLevel {
@@ -65,7 +95,7 @@ func NewLogger(format string, level string) zerolog.Logger {
}
func LoadConfig(v interface{}) {
if data != nil {
for _, data := range configs {
if err := yaml.Unmarshal(data, v); err != nil {
log.Warn().Err(err).Msg("[app] read config")
}
@@ -86,8 +116,18 @@ func GetLogger(module string) zerolog.Logger {
// internal
// data - config content
var data []byte
type Config []string
func (c *Config) String() string {
return strings.Join(*c, " ")
}
func (c *Config) Set(value string) error {
*c = append(*c, value)
return nil
}
var configs [][]byte
// modules log levels
var modules map[string]string
-10
View File
@@ -4,24 +4,14 @@ import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"net/http"
"os"
"strconv"
)
func Init() {
api.HandleFunc("api/stack", stackHandler)
api.HandleFunc("api/exit", exitHandler)
streams.HandleFunc("null", nullHandler)
}
func exitHandler(_ http.ResponseWriter, r *http.Request) {
s := r.URL.Query().Get("code")
code, _ := strconv.Atoi(s)
os.Exit(code)
}
func nullHandler(string) (streamer.Producer, error) {
return nil, nil
}
+1
View File
@@ -25,6 +25,7 @@ var stackSkip = [][]byte{
// webrtc/api.go
[]byte("created by github.com/pion/ice/v2.NewTCPMuxDefault"),
[]byte("created by github.com/pion/ice/v2.NewUDPMuxDefault"),
}
func stackHandler(w http.ResponseWriter, r *http.Request) {
+14 -3
View File
@@ -34,8 +34,13 @@ func Init() {
return false
}
waiter <- conn
return true
// unblocking write to channel
select {
case waiter <- conn:
return true
default:
return false
}
})
streams.HandleFunc("exec", Handle)
@@ -86,7 +91,13 @@ func Handle(url string) (streamer.Producer, error) {
chErr := make(chan error)
go func() {
chErr <- cmd.Wait()
err := cmd.Wait()
// unblocking write to channel
select {
case chErr <- err:
default:
log.Trace().Str("url", url).Msg("[exec] close")
}
}()
select {
+4 -6
View File
@@ -15,11 +15,11 @@ func deviceInputSuffix(videoIdx, audioIdx int) string {
audio := findMedia(streamer.KindAudio, audioIdx)
switch {
case video != nil && audio != nil:
return `"` + video.Title + `:` + audio.Title + `"`
return `"` + video.MID + `:` + audio.MID + `"`
case video != nil:
return `"` + video.Title + `"`
return `"` + video.MID + `"`
case audio != nil:
return `"` + audio.Title + `"`
return `"` + audio.MID + `"`
}
return ""
}
@@ -57,7 +57,5 @@ process:
}
func loadMedia(kind, name string) *streamer.Media {
return &streamer.Media{
Kind: kind, Title: name,
}
return &streamer.Media{Kind: kind, MID: name}
}
+2 -4
View File
@@ -13,7 +13,7 @@ const deviceInputPrefix = "-f v4l2"
func deviceInputSuffix(videoIdx, audioIdx int) string {
video := findMedia(streamer.KindVideo, videoIdx)
return video.Title
return video.MID
}
func loadMedias() {
@@ -44,7 +44,5 @@ func loadMedia(kind, name string) *streamer.Media {
return nil
}
return &streamer.Media{
Kind: kind, Title: name,
}
return &streamer.Media{Kind: kind, MID: name}
}
+4 -6
View File
@@ -15,11 +15,11 @@ func deviceInputSuffix(videoIdx, audioIdx int) string {
audio := findMedia(streamer.KindAudio, audioIdx)
switch {
case video != nil && audio != nil:
return `video="` + video.Title + `":audio=` + audio.Title + `"`
return `video="` + video.MID + `":audio=` + audio.MID + `"`
case video != nil:
return `video="` + video.Title + `"`
return `video="` + video.MID + `"`
case audio != nil:
return `audio="` + audio.Title + `"`
return `audio="` + audio.MID + `"`
}
return ""
}
@@ -53,7 +53,5 @@ func loadMedias() {
}
func loadMedia(kind, name string) *streamer.Media {
return &streamer.Media{
Kind: kind, Title: name,
}
return &streamer.Media{Kind: kind, MID: name}
}
+286 -172
View File
@@ -1,6 +1,8 @@
package ffmpeg
import (
"bytes"
"errors"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/exec"
"github.com/AlexxIT/go2rtc/cmd/ffmpeg/device"
@@ -17,190 +19,229 @@ func Init() {
Mod map[string]string `yaml:"ffmpeg"`
}
// defaults
cfg.Mod = map[string]string{
"bin": "ffmpeg",
// inputs
"file": "-re -stream_loop -1 -i {input}",
"http": "-fflags nobuffer -flags low_delay -i {input}",
"rtsp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i {input}",
// output
"output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
// `-g 30` - group of picture, GOP, keyframe interval
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
// `-tune zerolatency` - for minimal latency
// `-profile main -level 4.1` - most used streaming profile
// `-pix_fmt yuv420p` - if input pix format 4:2:2
"h264": "-c:v libx264 -g:v 30 -preset:v superfast -tune:v zerolatency -profile:v main -level:v 4.1 -pix_fmt:v yuv420p",
"h264/ultra": "-c:v libx264 -g:v 30 -preset:v ultrafast -tune:v zerolatency",
"h264/high": "-c:v libx264 -g:v 30 -preset:v superfast -tune:v zerolatency",
"h265": "-c:v libx265 -g:v 30 -preset:v superfast -tune:v zerolatency -profile:v main -level:v 5.1 -pix_fmt:v yuv420p",
"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
"opus": "-c:a libopus -ar:a 48000 -ac:a 2",
"pcmu": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
"pcmu/16000": "-c:a pcm_mulaw -ar:a 16000 -ac:a 1",
"pcmu/48000": "-c:a pcm_mulaw -ar:a 48000 -ac:a 1",
"pcma": "-c:a pcm_alaw -ar:a 8000 -ac:a 1",
"pcma/16000": "-c:a pcm_alaw -ar:a 16000 -ac:a 1",
"pcma/48000": "-c:a pcm_alaw -ar:a 48000 -ac:a 1",
"aac": "-c:a aac", // keep sample rate and channels
"aac/16000": "-c:a aac -ar:a 16000 -ac:a 1",
}
cfg.Mod = defaults // will be overriden from yaml
app.LoadConfig(&cfg)
tpl := cfg.Mod
cmd := "exec:" + tpl["bin"] + " -hide_banner "
if app.GetLogger("exec").GetLevel() >= 0 {
cmd += "-v error "
defaults["global"] += " -v error"
}
streams.HandleFunc("ffmpeg", func(s string) (streamer.Producer, error) {
s = s[7:] // remove `ffmpeg:`
var query url.Values
var queryVideo, queryAudio bool
if i := strings.IndexByte(s, '#'); i > 0 {
query = parseQuery(s[i+1:])
queryVideo = query["video"] != nil
queryAudio = query["audio"] != nil
s = s[:i]
} else {
// by default query both video and audio
queryVideo = true
queryAudio = true
streams.HandleFunc("ffmpeg", func(url string) (streamer.Producer, error) {
args := parseArgs(url[7:]) // remove `ffmpeg:`
if args == nil {
return nil, errors.New("can't generate ffmpeg command")
}
var input string
if i := strings.Index(s, "://"); i > 0 {
switch s[:i] {
case "http", "https", "rtmp":
input = strings.Replace(tpl["http"], "{input}", s, 1)
case "rtsp", "rtsps":
// https://ffmpeg.org/ffmpeg-protocols.html#rtsp
// skip unnecessary input tracks
switch {
case queryVideo && queryAudio:
input = "-allowed_media_types video+audio "
case queryVideo:
input = "-allowed_media_types video "
case queryAudio:
input = "-allowed_media_types audio "
}
input += strings.Replace(tpl["rtsp"], "{input}", s, 1)
default:
input = "-i " + s
}
} else if streams.Get(s) != nil {
s = "rtsp://localhost:" + rtsp.Port + "/" + s
switch {
case queryVideo && !queryAudio:
s += "?video"
case queryAudio && !queryVideo:
s += "?audio"
}
input = strings.Replace(tpl["rtsp"], "{input}", s, 1)
} else if strings.HasPrefix(s, "device?") {
var err error
input, err = device.GetInput(s)
if err != nil {
return nil, err
}
} else {
input = strings.Replace(tpl["file"], "{input}", s, 1)
}
if _, ok := query["async"]; ok {
input = "-use_wallclock_as_timestamps 1 -async 1 " + input
}
s = cmd + input
if query != nil {
for _, raw := range query["raw"] {
s += " " + raw
}
for _, rotate := range query["rotate"] {
switch rotate {
case "90":
s += " -vf transpose=1" // 90 degrees clockwise
case "180":
s += " -vf transpose=1,transpose=1"
case "-90", "270":
s += " -vf transpose=2" // 90 degrees counterclockwise
}
break
}
switch len(query["video"]) {
case 0:
s += " -vn"
case 1:
if len(query["audio"]) > 1 {
s += " -map 0:v:0?"
}
for _, video := range query["video"] {
if video == "copy" {
s += " -c:v copy"
} else {
s += " " + tpl[video]
}
}
default:
for i, video := range query["video"] {
if video == "copy" {
s += " -map 0:v:0? -c:v:" + strconv.Itoa(i) + " copy"
} else {
s += " -map 0:v:0? " + strings.ReplaceAll(tpl[video], ":v ", ":v:"+strconv.Itoa(i)+" ")
}
}
}
switch len(query["audio"]) {
case 0:
s += " -an"
case 1:
if len(query["video"]) > 1 {
s += " -map 0:a:0?"
}
for _, audio := range query["audio"] {
if audio == "copy" {
s += " -c:a copy"
} else {
s += " " + tpl[audio]
}
}
default:
for i, audio := range query["audio"] {
if audio == "copy" {
s += " -map 0:a:0? -c:a:" + strconv.Itoa(i) + " copy"
} else {
s += " -map 0:a:0? " + strings.ReplaceAll(tpl[audio], ":a ", ":a:"+strconv.Itoa(i)+" ")
}
}
}
} else {
s += " -c copy"
}
s += " " + tpl["output"]
return exec.Handle(s)
return exec.Handle("exec:" + args.String())
})
device.Bin = cfg.Mod["bin"]
device.Bin = defaults["bin"]
device.Init()
}
var defaults = map[string]string{
"bin": "ffmpeg",
"global": "-hide_banner",
// inputs
"file": "-re -stream_loop -1 -i {input}",
"http": "-fflags nobuffer -flags low_delay -i {input}",
"rtsp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i {input}",
// output
"output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
// `-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",
"h265": "-c:v libx265 -g 50 -profile:v high -level:v 5.1 -preset:v superfast -tune:v zerolatency",
"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
"opus": "-c:a libopus -ar:a 48000 -ac:a 2",
"pcmu": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
"pcmu/16000": "-c:a pcm_mulaw -ar:a 16000 -ac:a 1",
"pcmu/48000": "-c:a pcm_mulaw -ar:a 48000 -ac:a 1",
"pcma": "-c:a pcm_alaw -ar:a 8000 -ac:a 1",
"pcma/16000": "-c:a pcm_alaw -ar:a 16000 -ac:a 1",
"pcma/48000": "-c:a pcm_alaw -ar:a 48000 -ac:a 1",
"aac": "-c:a aac", // keep sample rate and channels
"aac/16000": "-c:a aac -ar:a 16000 -ac:a 1",
"mp3": "-c:a libmp3lame -q:a 8",
// hardware Intel and AMD on Linux
// better not to set `-async_depth:v 1` like for QSV, because framedrops
// `-bf 0` - disable B-frames is very important
"h264/vaapi": "-c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0",
"h265/vaapi": "-c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0",
"mjpeg/vaapi": "-c:v mjpeg_vaapi",
// hardware Raspberry
"h264/v4l2m2m": "-c:v h264_v4l2m2m -g 50 -bf 0",
"h265/v4l2m2m": "-c:v hevc_v4l2m2m -g 50 -bf 0",
// hardware NVidia on Linux and Windows
// preset=p2 - faster, tune=ll - low latency
"h264/cuda": "-c:v h264_nvenc -g 50 -profile:v high -level:v auto -preset:v p2 -tune:v ll",
"h265/cuda": "-c:v hevc_nvenc -g 50 -profile:v high -level:v auto",
// hardware Intel on Windows
"h264/dxva2": "-c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1",
"h265/dxva2": "-c:v hevc_qsv -g 50 -bf 0 -profile:v high -level:v 5.1 -async_depth:v 1",
"mjpeg/dxva2": "-c:v mjpeg_qsv -profile:v high -level:v 5.1",
// hardware macOS
"h264/videotoolbox": "-c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1",
"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1",
}
func parseArgs(s string) *Args {
// init FFmpeg arguments
args := &Args{
bin: defaults["bin"],
global: defaults["global"],
output: defaults["output"],
}
var query url.Values
if i := strings.IndexByte(s, '#'); i > 0 {
query = parseQuery(s[i+1:])
args.video = len(query["video"])
args.audio = len(query["audio"])
s = s[:i]
}
// Parse input:
// 1. Input as xxxx:// link (http or rtsp or any other)
// 2. Input as stream name
// 3. Input as FFmpeg device (local USB camera)
if i := strings.Index(s, "://"); i > 0 {
switch s[:i] {
case "http", "https", "rtmp":
args.input = strings.Replace(defaults["http"], "{input}", s, 1)
case "rtsp", "rtsps":
// https://ffmpeg.org/ffmpeg-protocols.html#rtsp
// skip unnecessary input tracks
switch {
case (args.video > 0 && args.audio > 0) || (args.video == 0 && args.audio == 0):
args.input = "-allowed_media_types video+audio "
case args.video > 0:
args.input = "-allowed_media_types video "
case args.audio > 0:
args.input = "-allowed_media_types audio "
}
args.input += strings.Replace(defaults["rtsp"], "{input}", s, 1)
default:
args.input = "-i " + s
}
} else if streams.Get(s) != nil {
s = "rtsp://localhost:" + rtsp.Port + "/" + s
switch {
case args.video > 0 && args.audio == 0:
s += "?video"
case args.audio > 0 && args.video == 0:
s += "?audio"
default:
s += "?video&audio"
}
args.input = strings.Replace(defaults["rtsp"], "{input}", s, 1)
} else if strings.HasPrefix(s, "device?") {
var err error
args.input, err = device.GetInput(s)
if err != nil {
return nil
}
} else {
args.input = strings.Replace(defaults["file"], "{input}", s, 1)
}
if query["async"] != nil {
args.input = "-use_wallclock_as_timestamps 1 -async 1 " + args.input
}
// Parse query params:
// 1. `width`/`height` params
// 2. `rotate` param
// 3. `video` params (support multiple)
// 4. `audio` params (support multiple)
// 5. `hardware` param
if query != nil {
// 1. Process raw params for FFmpeg
for _, raw := range query["raw"] {
args.AddCodec(raw)
}
// 2. Process video filters (resize and rotation)
if query["width"] != nil || query["height"] != nil {
filter := "scale="
if query["width"] != nil {
filter += query["width"][0]
} else {
filter += "-1"
}
filter += ":"
if query["height"] != nil {
filter += query["height"][0]
} else {
filter += "-1"
}
args.AddFilter(filter)
}
if query["rotate"] != nil {
var filter string
switch query["rotate"][0] {
case "90":
filter = "transpose=1" // 90 degrees clockwise
case "180":
filter = "transpose=1,transpose=1"
case "-90", "270":
filter = "transpose=2" // 90 degrees counterclockwise
}
if filter != "" {
args.AddFilter(filter)
}
}
// 3. Process video codecs
if args.video > 0 {
for _, video := range query["video"] {
if video != "copy" {
args.AddCodec(defaults[video])
} else {
args.AddCodec("-c:v copy")
}
}
} else {
args.AddCodec("-vn")
}
// 4. Process audio codecs
if args.audio > 0 {
for _, audio := range query["audio"] {
if audio != "copy" {
args.AddCodec(defaults[audio])
} else {
args.AddCodec("-c:a copy")
}
}
} else {
args.AddCodec("-an")
}
if query["hardware"] != nil {
MakeHardware(args, query["hardware"][0])
}
}
if args.codecs == nil {
args.AddCodec("-c copy")
}
return args
}
func parseQuery(s string) map[string][]string {
query := map[string][]string{}
for _, key := range strings.Split(s, "#") {
@@ -213,3 +254,76 @@ func parseQuery(s string) map[string][]string {
}
return query
}
type Args struct {
bin string // ffmpeg
global string // -hide_banner -v error
input string // -re -stream_loop -1 -i /media/bunny.mp4
codecs []string // -c:v libx264 -g:v 30 -preset:v ultrafast -tune:v zerolatency
filters []string // scale=1920:1080
output string // -f rtsp {output}
video, audio int // count of video and audio params
}
func (a *Args) AddCodec(codec string) {
a.codecs = append(a.codecs, codec)
}
func (a *Args) AddFilter(filter string) {
a.filters = append(a.filters, filter)
}
func (a *Args) InsertFilter(filter string) {
a.filters = append([]string{filter}, a.filters...)
}
func (a *Args) String() string {
b := bytes.NewBuffer(make([]byte, 0, 512))
b.WriteString(a.bin)
if a.global != "" {
b.WriteByte(' ')
b.WriteString(a.global)
}
b.WriteByte(' ')
b.WriteString(a.input)
multimode := a.video > 1 || a.audio > 1
var iv, ia int
for _, codec := range a.codecs {
// support multiple video and/or audio codecs
if multimode && len(codec) >= 5 {
switch codec[:5] {
case "-c:v ":
codec = "-map 0:v:0? " + strings.ReplaceAll(codec, ":v ", ":v:"+strconv.Itoa(iv)+" ")
iv++
case "-c:a ":
codec = "-map 0:a:0? " + strings.ReplaceAll(codec, ":a ", ":a:"+strconv.Itoa(ia)+" ")
ia++
}
}
b.WriteByte(' ')
b.WriteString(codec)
}
if a.filters != nil {
for i, filter := range a.filters {
if i == 0 {
b.WriteString(" -vf ")
} else {
b.WriteByte(',')
}
b.WriteString(filter)
}
}
b.WriteByte(' ')
b.WriteString(a.output)
return b.String()
}
+112
View File
@@ -0,0 +1,112 @@
package ffmpeg
import (
"github.com/rs/zerolog/log"
"os/exec"
"strings"
)
const (
EngineSoftware = "software"
EngineVAAPI = "vaapi" // Intel iGPU and AMD GPU
EngineV4L2M2M = "v4l2m2m" // Raspberry Pi 3 and 4
EngineCUDA = "cuda" // NVidia on Windows and Linux
EngineDXVA2 = "dxva2" // Intel on Windows
EngineVideoToolbox = "videotoolbox" // macOS
)
var cache = map[string]string{}
// MakeHardware converts software FFmpeg args to hardware args
// empty engine for autoselect
func MakeHardware(args *Args, engine string) {
for i, codec := range args.codecs {
if len(codec) < 12 {
continue // skip short line (-c:v libx264...)
}
// get current codec name
name := cut(codec, ' ', 1)
switch name {
case "libx264":
name = "h264"
case "libx265":
name = "h265"
case "mjpeg":
default:
continue // skip unsupported codec
}
// temporary disable probe for H265 and MJPEG
if engine == "" && name == "h264" {
if engine = cache[name]; engine == "" {
engine = ProbeHardware(name)
cache[name] = engine
}
}
switch engine {
case EngineVAAPI:
args.input = "-hwaccel vaapi -hwaccel_output_format vaapi " + args.input
args.codecs[i] = defaults[name+"/"+engine]
for i, filter := range args.filters {
if strings.HasPrefix(filter, "scale=") {
args.filters[i] = "scale_vaapi=" + filter[6:]
}
}
// fix if input doesn't support hwaccel, do nothing when support
args.InsertFilter("format=vaapi|nv12,hwupload")
case EngineCUDA:
args.input = "-hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 2 " + args.input
args.codecs[i] = defaults[name+"/"+engine]
for i, filter := range args.filters {
if strings.HasPrefix(filter, "scale=") {
args.filters[i] = "scale_cuda=" + filter[6:]
}
}
case EngineDXVA2:
args.input = "-hwaccel dxva2 -hwaccel_output_format dxva2_vld " + args.input
args.codecs[i] = defaults[name+"/"+engine]
for i, filter := range args.filters {
if strings.HasPrefix(filter, "scale=") {
args.filters[i] = "scale_qsv=" + filter[6:]
}
}
args.InsertFilter("hwmap=derive_device=qsv,format=qsv")
case EngineVideoToolbox:
args.input = "-hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld " + args.input
args.codecs[i] = defaults[name+"/"+engine]
case EngineV4L2M2M:
args.codecs[i] = defaults[name+"/"+engine]
}
}
}
func run(arg ...string) bool {
err := exec.Command(defaults["bin"], arg...).Run()
log.Printf("%v %v", arg, err)
return err == nil
}
func cut(s string, sep byte, pos int) string {
for n := 0; n < pos; n++ {
if i := strings.IndexByte(s, sep); i > 0 {
s = s[i+1:]
} else {
return ""
}
}
if i := strings.IndexByte(s, sep); i > 0 {
return s[:i]
}
return s
}
+21
View File
@@ -0,0 +1,21 @@
package ffmpeg
func ProbeHardware(name string) string {
switch name {
case "h264":
if run(
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "h264_videotoolbox", "-f", "null", "-") {
return EngineVideoToolbox
}
case "h265":
if run(
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "hevc_videotoolbox", "-f", "null", "-") {
return EngineVideoToolbox
}
}
return EngineSoftware
}
+67
View File
@@ -0,0 +1,67 @@
package ffmpeg
import (
"runtime"
)
func ProbeHardware(name string) string {
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
switch name {
case "h264":
if run(
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "h264_v4l2m2m", "-f", "null", "-") {
return EngineV4L2M2M
}
case "h265":
if run(
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "hevc_v4l2m2m", "-f", "null", "-") {
return EngineV4L2M2M
}
}
return EngineSoftware
}
switch name {
case "h264":
if run("-init_hw_device", "cuda",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "h264_nvenc", "-f", "null", "-") {
return EngineCUDA
}
if run("-init_hw_device", "vaapi",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-vf", "format=nv12,hwupload",
"-c", "h264_vaapi", "-f", "null", "-") {
return EngineVAAPI
}
case "h265":
if run("-init_hw_device", "cuda",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "hevc_nvenc", "-f", "null", "-") {
return EngineCUDA
}
if run("-init_hw_device", "vaapi",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-vf", "format=nv12,hwupload",
"-c", "hevc_vaapi", "-f", "null", "-") {
return EngineVAAPI
}
case "mjpeg":
if run("-init_hw_device", "vaapi",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-vf", "format=nv12,hwupload",
"-c", "mjpeg_vaapi", "-f", "null", "-") {
return EngineVAAPI
}
}
return EngineSoftware
}
+40
View File
@@ -0,0 +1,40 @@
package ffmpeg
func ProbeHardware(name string) string {
switch name {
case "h264":
if run("-init_hw_device", "cuda",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "h264_nvenc", "-f", "null", "-") {
return EngineCUDA
}
if run("-init_hw_device", "dxva2",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "h264_qsv", "-f", "null", "-") {
return EngineDXVA2
}
case "h265":
if run("-init_hw_device", "cuda",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "hevc_nvenc", "-f", "null", "-") {
return EngineCUDA
}
if run("-init_hw_device", "dxva2",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "hevc_qsv", "-f", "null", "-") {
return EngineDXVA2
}
case "mjpeg":
if run("-init_hw_device", "dxva2",
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
"-c", "mjpeg_qsv", "-f", "null", "-") {
return EngineDXVA2
}
}
return EngineSoftware
}
+261
View File
@@ -0,0 +1,261 @@
package hls
import (
"fmt"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/ts"
"github.com/rs/zerolog/log"
"net/http"
"strconv"
"sync"
"time"
)
func Init() {
api.HandleFunc("api/stream.m3u8", handlerStream)
api.HandleFunc("api/hls/playlist.m3u8", handlerPlaylist)
// HLS (TS)
api.HandleFunc("api/hls/segment.ts", handlerSegmentTS)
// HLS (fMP4)
api.HandleFunc("api/hls/init.mp4", handlerInit)
api.HandleFunc("api/hls/segment.m4s", handlerSegmentMP4)
}
type Consumer interface {
streamer.Consumer
Init() ([]byte, error)
MimeCodecs() string
Start()
}
type Session struct {
cons Consumer
playlist string
init []byte
segment []byte
seq int
alive *time.Timer
mu sync.Mutex
}
const keepalive = 5 * time.Second
var sessions = map[string]*Session{}
func handlerStream(w http.ResponseWriter, r *http.Request) {
// CORS important for Chromecast
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "application/vnd.apple.mpegurl")
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Methods", "GET")
return
}
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
var cons Consumer
// use fMP4 with codecs filter and TS without
medias := mp4.ParseQuery(r.URL.Query())
if medias != nil {
cons = &mp4.Consumer{
RemoteAddr: r.RemoteAddr,
UserAgent: r.UserAgent(),
Medias: medias,
}
} else {
cons = &ts.Consumer{
RemoteAddr: r.RemoteAddr,
UserAgent: r.UserAgent(),
}
}
session := &Session{cons: cons}
cons.Listen(func(msg interface{}) {
if data, ok := msg.([]byte); ok {
session.mu.Lock()
session.segment = append(session.segment, data...)
session.mu.Unlock()
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return
}
session.alive = time.AfterFunc(keepalive, func() {
stream.RemoveConsumer(cons)
})
session.init, _ = cons.Init()
cons.Start()
sid := strconv.FormatInt(time.Now().UnixNano(), 10)
// two segments important for Chromecast
if medias != nil {
session.playlist = `#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:1
#EXT-X-MEDIA-SEQUENCE:%d
#EXT-X-MAP:URI="init.mp4?id=` + sid + `"
#EXTINF:0.500,
segment.m4s?id=` + sid + `&n=%d
#EXTINF:0.500,
segment.m4s?id=` + sid + `&n=%d`
} else {
session.playlist = `#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:1
#EXT-X-MEDIA-SEQUENCE:%d
#EXTINF:0.500,
segment.ts?id=` + sid + `&n=%d
#EXTINF:0.500,
segment.ts?id=` + sid + `&n=%d`
}
sessions[sid] = session
// bandwidth important for Safari, codecs useful for smooth playback
data := []byte(`#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS="` + cons.MimeCodecs() + `"
hls/playlist.m3u8?id=` + sid)
if _, err := w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
}
}
func handlerPlaylist(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "application/vnd.apple.mpegurl")
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Methods", "GET")
return
}
sid := r.URL.Query().Get("id")
session := sessions[sid]
if session == nil {
http.NotFound(w, r)
return
}
s := fmt.Sprintf(session.playlist, session.seq, session.seq, session.seq+1)
if _, err := w.Write([]byte(s)); err != nil {
log.Error().Err(err).Caller().Send()
}
}
func handlerSegmentTS(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "video/mp2t")
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Methods", "GET")
return
}
sid := r.URL.Query().Get("id")
session := sessions[sid]
if session == nil {
http.NotFound(w, r)
return
}
session.alive.Reset(keepalive)
var i byte
for len(session.segment) == 0 {
if i++; i > 10 {
http.NotFound(w, r)
return
}
time.Sleep(time.Millisecond * 100)
}
session.mu.Lock()
data := session.segment
// important to start new segment with init
session.segment = session.init
session.seq++
session.mu.Unlock()
if _, err := w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
}
}
func handlerInit(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Add("Content-Type", "video/mp4")
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Methods", "GET")
return
}
sid := r.URL.Query().Get("id")
session := sessions[sid]
if session == nil {
http.NotFound(w, r)
return
}
if _, err := w.Write(session.init); err != nil {
log.Error().Err(err).Caller().Send()
}
}
func handlerSegmentMP4(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Add("Content-Type", "video/iso.segment")
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
return
}
sid := r.URL.Query().Get("id")
session := sessions[sid]
if session == nil {
http.NotFound(w, r)
return
}
session.alive.Reset(keepalive)
var i byte
for len(session.segment) == 0 {
if i++; i > 10 {
http.NotFound(w, r)
return
}
time.Sleep(time.Millisecond * 100)
}
session.mu.Lock()
data := session.segment
session.segment = nil
session.seq++
session.mu.Unlock()
if _, err := w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
}
}
+14 -3
View File
@@ -27,7 +27,10 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
exit := make(chan []byte)
cons := &mjpeg.Consumer{}
cons := &mjpeg.Consumer{
RemoteAddr: r.RemoteAddr,
UserAgent: r.UserAgent(),
}
cons.Listen(func(msg interface{}) {
switch msg := msg.(type) {
case []byte:
@@ -68,7 +71,10 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
flusher := w.(http.Flusher)
cons := &mjpeg.Consumer{}
cons := &mjpeg.Consumer{
RemoteAddr: r.RemoteAddr,
UserAgent: r.UserAgent(),
}
cons.Listen(func(msg interface{}) {
switch msg := msg.(type) {
case []byte:
@@ -109,7 +115,10 @@ func handlerWS(tr *api.Transport, _ *api.Message) error {
return errors.New(api.StreamNotFound)
}
cons := &mjpeg.Consumer{}
cons := &mjpeg.Consumer{
RemoteAddr: tr.Request.RemoteAddr,
UserAgent: tr.Request.UserAgent(),
}
cons.Listen(func(msg interface{}) {
if data, ok := msg.([]byte); ok {
tr.Write(data)
@@ -121,6 +130,8 @@ func handlerWS(tr *api.Transport, _ *api.Message) error {
return err
}
tr.Write(&api.Message{Type: "mjpeg"})
tr.OnClose(func() {
stream.RemoveConsumer(cons)
})
+31 -24
View File
@@ -5,6 +5,7 @@ import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog"
"net/http"
"strconv"
@@ -25,8 +26,14 @@ func Init() {
var log zerolog.Logger
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
if isChromeFirst(w, r) {
return
// Chrome 105 does two requests: without Range and with `Range: bytes=0-`
ua := r.UserAgent()
if strings.Contains(ua, " Chrome/") {
if r.Header.Values("Range") == nil {
w.Header().Set("Content-Type", "video/mp4")
w.WriteHeader(http.StatusOK)
return
}
}
src := r.URL.Query().Get("src")
@@ -67,7 +74,22 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
func handlerMP4(w http.ResponseWriter, r *http.Request) {
log.Trace().Msgf("[mp4] %s %+v", r.Method, r.Header)
if isChromeFirst(w, r) || isSafari(w, r) {
// Chrome has Safari in UA, so check first Chrome and later Safari
ua := r.UserAgent()
if strings.Contains(ua, " Chrome/") {
if r.Header.Values("Range") == nil {
w.Header().Set("Content-Type", "video/mp4")
w.WriteHeader(http.StatusOK)
return
}
} else if strings.Contains(ua, " Safari/") {
// auto redirect to HLS/fMP4 format, because Safari not support MP4 stream
url := "stream.m3u8?" + r.URL.RawQuery
if !r.URL.Query().Has("mp4") {
url += "&mp4"
}
http.Redirect(w, r, url, http.StatusMovedPermanently)
return
}
@@ -80,7 +102,12 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
exit := make(chan error)
cons := &mp4.Consumer{}
cons := &mp4.Consumer{
RemoteAddr: r.RemoteAddr,
UserAgent: r.UserAgent(),
Medias: streamer.ParseQuery(r.URL.Query()),
}
cons.Listen(func(msg interface{}) {
if data, ok := msg.([]byte); ok {
if _, err := w.Write(data); err != nil && exit != nil {
@@ -132,23 +159,3 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
duration.Stop()
}
}
func isChromeFirst(w http.ResponseWriter, r *http.Request) bool {
// Chrome 105 does two requests: without Range and with `Range: bytes=0-`
if strings.Contains(r.UserAgent(), " Chrome/") {
if r.Header.Values("Range") == nil {
w.Header().Set("Content-Type", "video/mp4")
w.WriteHeader(http.StatusOK)
return true
}
}
return false
}
func isSafari(w http.ResponseWriter, r *http.Request) bool {
if r.Header.Get("Range") == "bytes=0-1" {
handlerKeyframe(w, r)
return true
}
return false
}
+19 -9
View File
@@ -9,7 +9,7 @@ import (
"strings"
)
const packetSize = 8192
const packetSize = 1400
func handlerWSMSE(tr *api.Transport, msg *api.Message) error {
src := tr.Request.URL.Query().Get("src")
@@ -18,9 +18,10 @@ func handlerWSMSE(tr *api.Transport, msg *api.Message) error {
return errors.New(api.StreamNotFound)
}
cons := &mp4.Consumer{}
cons.UserAgent = tr.Request.UserAgent()
cons.RemoteAddr = tr.Request.RemoteAddr
cons := &mp4.Consumer{
RemoteAddr: tr.Request.RemoteAddr,
UserAgent: tr.Request.UserAgent(),
}
if codecs, ok := msg.Value.(string); ok {
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MSE consumer")
@@ -38,7 +39,7 @@ func handlerWSMSE(tr *api.Transport, msg *api.Message) error {
})
if err := stream.AddConsumer(cons); err != nil {
log.Warn().Err(err).Caller().Send()
log.Debug().Err(err).Msg("[mp4] add consumer")
return err
}
@@ -68,7 +69,11 @@ func handlerWSMP4(tr *api.Transport, msg *api.Message) error {
return errors.New(api.StreamNotFound)
}
cons := &mp4.Segment{}
cons := &mp4.Segment{
RemoteAddr: tr.Request.RemoteAddr,
UserAgent: tr.Request.UserAgent(),
OnlyKeyframe: true,
}
if codecs, ok := msg.Value.(string); ok {
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MP4 consumer")
@@ -86,6 +91,8 @@ func handlerWSMP4(tr *api.Transport, msg *api.Message) error {
return err
}
tr.Write(&api.Message{Type: "mp4", Value: cons.MimeType})
tr.OnClose(func() {
stream.RemoveConsumer(cons)
})
@@ -99,15 +106,18 @@ func parseMedias(codecs string, parseAudio bool) (medias []*streamer.Media) {
for _, name := range strings.Split(codecs, ",") {
switch name {
case "avc1.640029":
case mp4.MimeH264:
codec := &streamer.Codec{Name: streamer.CodecH264}
videos = append(videos, codec)
case "hvc1.1.6.L153.B0":
case mp4.MimeH265:
codec := &streamer.Codec{Name: streamer.CodecH265}
videos = append(videos, codec)
case "mp4a.40.2":
case mp4.MimeAAC:
codec := &streamer.Codec{Name: streamer.CodecAAC}
audios = append(audios, codec)
case mp4.MimeOpus:
codec := &streamer.Codec{Name: streamer.CodecOpus}
audios = append(audios, codec)
}
}
+27 -47
View File
@@ -3,27 +3,32 @@ package rtsp
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog"
"net"
"net/url"
"strings"
)
func Init() {
var conf struct {
Mod struct {
Listen string `yaml:"listen"`
Username string `yaml:"username"`
Password string `yaml:"password"`
Listen string `yaml:"listen" json:"listen"`
Username string `yaml:"username" json:"-"`
Password string `yaml:"password" json:"-"`
DefaultQuery string `yaml:"default_query" json:"default_query"`
} `yaml:"rtsp"`
}
// default config
conf.Mod.Listen = ":8554"
conf.Mod.DefaultQuery = "video&audio"
app.LoadConfig(&conf)
app.Info["rtsp"] = conf.Mod
log = app.GetLogger("rtsp")
@@ -48,6 +53,10 @@ func Init() {
log.Info().Str("addr", address).Msg("[rtsp] listen")
if query, err := url.ParseQuery(conf.Mod.DefaultQuery); err == nil {
defaultMedias = mp4.ParseQuery(query)
}
go func() {
for {
conn, err := ln.Accept()
@@ -77,6 +86,7 @@ var Port string
var log zerolog.Logger
var handlers []Handler
var defaultMedias []*streamer.Media
func rtspHandler(url string) (streamer.Producer, error) {
backchannel := true
@@ -102,6 +112,8 @@ func rtspHandler(url string) (streamer.Producer, error) {
log.Trace().Msgf("[rtsp] client request:\n%s", msg)
case *tcp.Response:
log.Trace().Msgf("[rtsp] client response:\n%s", msg)
case string:
log.Trace().Msgf("[rtsp] client msg: %s", msg)
}
})
}
@@ -161,7 +173,14 @@ func tcpHandler(conn *rtsp.Conn) {
log.Debug().Str("stream", name).Msg("[rtsp] new consumer")
initMedias(conn)
conn.SessionName = app.UserAgent
conn.Medias = mp4.ParseQuery(conn.URL.Query())
if conn.Medias == nil {
for _, media := range defaultMedias {
conn.Medias = append(conn.Medias, media.Clone())
}
}
if err := stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Str("stream", name).Msg("[rtsp]")
@@ -200,6 +219,9 @@ func tcpHandler(conn *rtsp.Conn) {
if err := conn.Accept(); err != nil {
log.Warn().Err(err).Caller().Send()
if closer != nil {
closer()
}
_ = conn.Close()
return
}
@@ -212,7 +234,7 @@ func tcpHandler(conn *rtsp.Conn) {
if closer != nil {
if err := conn.Handle(); err != nil {
log.Debug().Err(err).Caller().Send()
log.Debug().Msgf("[rtsp] handle=%s", err)
}
closer()
@@ -222,45 +244,3 @@ func tcpHandler(conn *rtsp.Conn) {
_ = conn.Close()
}
func initMedias(conn *rtsp.Conn) {
// set media candidates from query list
for key, value := range conn.URL.Query() {
switch key {
case streamer.KindVideo, streamer.KindAudio:
for _, name := range value {
name = strings.ToUpper(name)
// check aliases
switch name {
case "COPY":
name = "" // pass empty codecs list
case "MJPEG":
name = streamer.CodecJPEG
case "AAC":
name = streamer.CodecAAC
}
media := &streamer.Media{
Kind: key, Direction: streamer.DirectionRecvonly,
}
// empty codecs match all codecs
if name != "" {
// empty clock rate and channels match any values
media.Codecs = []*streamer.Codec{{Name: name}}
}
conn.Medias = append(conn.Medias, media)
}
}
}
// set default media candidates if query is empty
if conn.Medias == nil {
conn.Medias = []*streamer.Media{
{Kind: streamer.KindVideo, Direction: streamer.DirectionRecvonly},
{Kind: streamer.KindAudio, Direction: streamer.DirectionRecvonly},
}
}
}
+15
View File
@@ -0,0 +1,15 @@
package streams
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/streamer"
)
type Consumer struct {
element streamer.Consumer
tracks []*streamer.Track
}
func (c *Consumer) MarshalJSON() ([]byte, error) {
return json.Marshal(c.element)
}
+52 -31
View File
@@ -1,6 +1,7 @@
package streams
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
"sync"
@@ -24,11 +25,12 @@ type Producer struct {
template string
element streamer.Producer
lastErr error
tracks []*streamer.Track
state state
mu sync.Mutex
restart *time.Timer
state state
mu sync.Mutex
workerID int
}
func (p *Producer) SetSource(s string) {
@@ -45,16 +47,20 @@ func (p *Producer) GetMedias() []*streamer.Media {
if p.state == stateNone {
log.Debug().Msgf("[streams] probe producer url=%s", p.url)
var err error
p.element, err = GetProducer(p.url)
if err != nil || p.element == nil {
log.Error().Err(err).Caller().Send()
p.element, p.lastErr = GetProducer(p.url)
if p.lastErr != nil || p.element == nil {
log.Error().Err(p.lastErr).Str("url", p.url).Caller().Send()
return nil
}
p.state = stateMedias
}
// if element in reconnect state
if p.element == nil {
return nil
}
return p.element.GetMedias()
}
@@ -86,6 +92,15 @@ func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *strea
return track
}
func (p *Producer) MarshalJSON() ([]byte, error) {
if p.element != nil {
return json.Marshal(p.element)
}
info := streamer.Info{URL: p.url}
return json.Marshal(info)
}
// internals
func (p *Producer) start() {
@@ -99,32 +114,45 @@ func (p *Producer) start() {
log.Debug().Msgf("[streams] start producer url=%s", p.url)
p.state = stateStart
go func() {
// safe read element while mu locked
if err := p.element.Start(); err != nil {
log.Warn().Err(err).Caller().Send()
}
p.reconnect()
}()
p.workerID++
go p.worker(p.element, p.workerID)
}
func (p *Producer) reconnect() {
func (p *Producer) worker(element streamer.Producer, workerID int) {
if err := element.Start(); err != nil {
p.mu.Lock()
closed := p.workerID != workerID
p.mu.Unlock()
if closed {
return
}
log.Warn().Err(err).Str("url", p.url).Caller().Send()
}
p.reconnect(workerID)
}
func (p *Producer) reconnect(workerID int) {
p.mu.Lock()
defer p.mu.Unlock()
if p.state != stateStart {
if p.workerID != workerID {
log.Trace().Msgf("[streams] stop reconnect url=%s", p.url)
return
}
log.Debug().Msgf("[streams] reconnect to url=%s", p.url)
var err error
p.element, err = GetProducer(p.url)
if err != nil || p.element == nil {
log.Debug().Err(err).Caller().Send()
p.element, p.lastErr = GetProducer(p.url)
if p.lastErr != nil || p.element == nil {
log.Debug().Msgf("[streams] producer=%s", p.lastErr)
// TODO: dynamic timeout
p.restart = time.AfterFunc(30*time.Second, p.reconnect)
time.AfterFunc(30*time.Second, func() {
p.reconnect(workerID)
})
return
}
@@ -148,12 +176,7 @@ func (p *Producer) reconnect() {
}
}
go func() {
if err = p.element.Start(); err != nil {
log.Debug().Err(err).Caller().Send()
}
p.reconnect()
}()
go p.worker(p.element, workerID)
}
func (p *Producer) stop() {
@@ -167,6 +190,8 @@ func (p *Producer) stop() {
case stateNone:
log.Debug().Msgf("[streams] can't stop none producer")
return
case stateStart:
p.workerID++
}
log.Debug().Msgf("[streams] stop producer url=%s", p.url)
@@ -175,10 +200,6 @@ func (p *Producer) stop() {
_ = p.element.Stop()
p.element = nil
}
if p.restart != nil {
p.restart.Stop()
p.restart = nil
}
p.state = stateNone
p.tracks = nil
+37 -23
View File
@@ -7,17 +7,14 @@ import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
"sync"
"sync/atomic"
)
type Consumer struct {
element streamer.Consumer
tracks []*streamer.Track
}
type Stream struct {
producers []*Producer
consumers []*Consumer
mu sync.Mutex
requests int32
}
func NewStream(source interface{}) *Stream {
@@ -52,6 +49,9 @@ func (s *Stream) SetSource(source string) {
}
func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
// support for multiple simultaneous requests from different consumers
atomic.AddInt32(&s.requests, 1)
ic := len(s.consumers)
consumer := &Consumer{element: cons}
@@ -82,7 +82,7 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
// Step 4. Get producer track
prodTrack := prod.GetTrack(prodMedia, prodCodec)
if prodTrack == nil {
log.Warn().Msg("[stream] can't get track")
log.Warn().Str("url", prod.url).Msg("[streams] can't get track")
continue
}
@@ -91,19 +91,30 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
consumer.tracks = append(consumer.tracks, consTrack)
producers = append(producers, prod)
break producers
if !consMedia.MatchAll() {
break producers
}
}
}
}
}
if len(producers) == 0 {
if atomic.AddInt32(&s.requests, -1) == 0 {
s.stopProducers()
}
if len(producers) == 0 {
if len(codecs) > 0 {
return errors.New("codecs not match: " + codecs)
} else {
return fmt.Errorf("sources unavailable: %d", len(s.producers))
}
for i, producer := range s.producers {
if producer.lastErr != nil {
return fmt.Errorf("source %d error: %w", i, producer.lastErr)
}
}
return fmt.Errorf("sources unavailable: %d", len(s.producers))
}
s.mu.Lock()
@@ -183,22 +194,21 @@ producers:
//}
func (s *Stream) MarshalJSON() ([]byte, error) {
var v []interface{}
s.mu.Lock()
for _, prod := range s.producers {
if prod.element != nil {
v = append(v, prod.element)
}
if !s.mu.TryLock() {
log.Warn().Msgf("[streams] json locked")
return json.Marshal(nil)
}
for _, cons := range s.consumers {
// cons.element always not nil
v = append(v, cons.element)
var info struct {
Producers []*Producer `json:"producers"`
Consumers []*Consumer `json:"consumers"`
}
info.Producers = s.producers
info.Consumers = s.consumers
s.mu.Unlock()
if len(v) == 0 {
v = nil
}
return json.Marshal(v)
return json.Marshal(info)
}
func (s *Stream) removeConsumer(i int) {
@@ -228,6 +238,10 @@ func (s *Stream) removeProducer(i int) {
}
func collectCodecs(media *streamer.Media, codecs *string) {
if media.Direction == streamer.DirectionRecvonly {
return
}
for _, codec := range media.Codecs {
name := codec.Name
if name == streamer.CodecAAC {
+26 -11
View File
@@ -1,9 +1,12 @@
package streams
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/app/store"
"github.com/rs/zerolog"
"net/http"
)
func Init() {
@@ -22,6 +25,8 @@ func Init() {
for name, item := range store.GetDict("streams") {
streams[name] = NewStream(item)
}
api.HandleFunc("api/streams", streamsHandler)
}
func Get(name string) *Stream {
@@ -48,19 +53,29 @@ func GetOrNew(src string) *Stream {
return New(src, src)
}
func Delete(name string) {
delete(streams, name)
}
func streamsHandler(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
func All() map[string]interface{} {
all := map[string]interface{}{}
for name, stream := range streams {
all[name] = stream
//if stream.Active() {
// all[name] = stream
//}
switch r.Method {
case "PUT":
name := r.URL.Query().Get("name")
if name == "" {
name = src
}
New(name, src)
return
case "DELETE":
delete(streams, src)
return
}
if src != "" {
e := json.NewEncoder(w)
e.SetIndent("", " ")
_ = e.Encode(streams[src])
} else {
_ = json.NewEncoder(w).Encode(streams)
}
return all
}
var log zerolog.Logger
+19 -14
View File
@@ -7,6 +7,7 @@ import (
)
var candidates []string
var networks = []string{"udp", "tcp"}
func AddCandidate(address string) {
candidates = append(candidates, address)
@@ -20,15 +21,17 @@ func asyncCandidates(tr *api.Transport) {
continue
}
cand, err := webrtc.NewCandidate(address)
if err != nil {
log.Warn().Err(err).Caller().Send()
continue
for _, network := range networks {
cand, err := webrtc.NewCandidate(network, address)
if err != nil {
log.Warn().Err(err).Caller().Send()
continue
}
log.Trace().Str("candidate", cand).Msg("[webrtc] config")
tr.Write(&api.Message{Type: "webrtc/candidate", Value: cand})
}
log.Trace().Str("candidate", cand).Msg("[webrtc] config")
tr.Write(&api.Message{Type: "webrtc/candidate", Value: cand})
}
}
@@ -57,13 +60,15 @@ func syncCanditates(answer string) (string, error) {
continue
}
cand, err := webrtc.NewCandidate(address)
if err != nil {
log.Warn().Err(err).Msg("[webrtc] candidate")
continue
}
for _, network := range networks {
cand, err := webrtc.NewCandidate(network, address)
if err != nil {
log.Warn().Err(err).Msg("[webrtc] candidate")
continue
}
md.WithPropertyAttribute(cand)
md.WithPropertyAttribute(cand)
}
}
if end {
+3 -2
View File
@@ -22,6 +22,7 @@ func Init() {
} `yaml:"webrtc"`
}
cfg.Mod.Listen = ":8555"
cfg.Mod.IceServers = []pion.ICEServer{
{URLs: []string{"stun:stun.l.google.com:19302"}},
}
@@ -68,7 +69,7 @@ var NewPConn func() (*pion.PeerConnection, error)
func asyncHandler(tr *api.Transport, msg *api.Message) error {
src := tr.Request.URL.Query().Get("src")
stream := streams.Get(src)
stream := streams.GetOrNew(src)
if stream == nil {
return errors.New(api.StreamNotFound)
}
@@ -112,7 +113,7 @@ func asyncHandler(tr *api.Transport, msg *api.Message) error {
// 2. AddConsumer, so we get new tracks
if err = stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Caller().Send()
log.Debug().Err(err).Msg("[webrtc] add consumer")
_ = conn.Conn.Close()
return err
}
+52
View File
@@ -0,0 +1,52 @@
# 0. Prepare images
# only debian 12 (bookworm) has latest ffmpeg
ARG DEBIAN_VERSION="bookworm-slim"
ARG GO_VERSION="1.19-buster"
ARG NGROK_VERSION="3"
FROM debian:${DEBIAN_VERSION} AS base
FROM golang:${GO_VERSION} AS go
FROM ngrok/ngrok:${NGROK_VERSION} AS ngrok
# 1. Build go2rtc binary
FROM go AS build
WORKDIR /build
# Cache dependencies
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
# 2. Collect all files
FROM scratch AS rootfs
COPY --from=build /build/go2rtc /usr/local/bin/
COPY --from=ngrok /bin/ngrok /usr/local/bin/
COPY ./build/docker/run.sh /
# 3. Final image
FROM base
# Install ffmpeg, bash (for run.sh), tini (for signal handling),
# and other common tools for the echo source.
# non-free for Intel QSV support (not used by go2rtc, just for tests)
RUN echo 'deb http://deb.debian.org/debian bookworm non-free' > /etc/apt/sources.list.d/debian-non-free.list && \
apt-get -y update && apt-get -y install tini ffmpeg python3 curl jq intel-media-va-driver-non-free
COPY --from=rootfs / /
RUN chmod a+x /run.sh && mkdir -p /config
ENTRYPOINT ["/usr/bin/tini", "--"]
# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
ENV NVIDIA_VISIBLE_DEVICES all
ENV NVIDIA_DRIVER_CAPABILITIES compute,video,utility
CMD ["/run.sh"]
+9 -10
View File
@@ -8,6 +8,7 @@ import (
"github.com/AlexxIT/go2rtc/cmd/exec"
"github.com/AlexxIT/go2rtc/cmd/ffmpeg"
"github.com/AlexxIT/go2rtc/cmd/hass"
"github.com/AlexxIT/go2rtc/cmd/hls"
"github.com/AlexxIT/go2rtc/cmd/homekit"
"github.com/AlexxIT/go2rtc/cmd/http"
"github.com/AlexxIT/go2rtc/cmd/ivideon"
@@ -26,27 +27,25 @@ import (
func main() {
app.Init() // init config and logs
api.Init() // init HTTP API server
streams.Init() // load streams list
api.Init() // init HTTP API server
echo.Init()
rtsp.Init() // add support RTSP client and RTSP server
rtmp.Init() // add support RTMP client
exec.Init() // add support exec scheme (depends on RTSP server)
ffmpeg.Init() // add support ffmpeg scheme (depends on exec scheme)
hass.Init() // add support hass scheme
webrtc.Init()
mp4.Init()
mjpeg.Init()
http.Init()
echo.Init()
ivideon.Init()
srtp.Init()
homekit.Init()
ivideon.Init()
webrtc.Init()
mp4.Init()
hls.Init()
mjpeg.Init()
http.Init()
ngrok.Init()
debug.Init()
+10 -1
View File
@@ -17,9 +17,14 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
//log.Printf("[RTP/AAC] units: %d, size: %4d, ts: %10d, %t", headersSize/2, len(packet.Payload), packet.Timestamp, packet.Marker)
data := packet.Payload[2+headersSize:]
if IsADTS(data) {
data = data[7:]
}
clone := *packet
clone.Version = RTPPacketVersionAAC
clone.Payload = packet.Payload[2+headersSize:]
clone.Payload = data
return push(&clone)
}
}
@@ -55,3 +60,7 @@ func RTPPay(mtu uint16) streamer.WrapperFunc {
}
}
}
func IsADTS(b []byte) bool {
return len(b) > 7 && b[0] == 0xFF && b[1]&0xF0 == 0xF0
}
+33 -3
View File
@@ -3,6 +3,8 @@ package h264
import (
"encoding/base64"
"encoding/binary"
"encoding/hex"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
)
@@ -47,11 +49,39 @@ func Join(ps, iframe []byte) []byte {
return b
}
// GetProfileLevelID - get profile from fmtp line
// Some devices won't play video with high level, so limit max profile and max level.
// And return some profile even if fmtp line is empty.
func GetProfileLevelID(fmtp string) string {
if fmtp == "" {
return ""
// avc1.640029 - H.264 high 4.1 (Chromecast 1st and 2nd Gen)
profile := byte(0x64)
capab := byte(0)
level := byte(0x29)
if fmtp != "" {
var conf []byte
// some cameras has wrong profile-level-id
// https://github.com/AlexxIT/go2rtc/issues/155
if s := streamer.Between(fmtp, "sprop-parameter-sets=", ","); s != "" {
if sps, _ := base64.StdEncoding.DecodeString(s); len(sps) >= 4 {
conf = sps[1:4]
}
} else if s = streamer.Between(fmtp, "profile-level-id=", ";"); s != "" {
conf, _ = hex.DecodeString(s)
}
if conf != nil {
if conf[0] < profile {
profile = conf[0]
capab = conf[1]
}
if conf[2] < level {
level = conf[2]
}
}
}
return streamer.Between(fmtp, "profile-level-id=", ";")
return fmt.Sprintf("%02X%02X%02X", profile, capab, level)
}
func GetParameterSet(fmtp string) (sps, pps []byte) {
+52 -20
View File
@@ -1,6 +1,7 @@
package h264
import (
"bytes"
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
@@ -9,6 +10,8 @@ import (
const RTPPacketVersionAVC = 0
const PSMaxSize = 128 // the biggest SPS I've seen is 48 (EZVIZ CS-CV210)
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
depack := &codecs.H264Packet{IsAVC: true}
@@ -27,35 +30,40 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
}
// Fix TP-Link Tapo TC70: sends SPS and PPS with packet.Marker = true
if packet.Marker {
// Reolink Duo 2: sends SPS with Marker and PPS without
if packet.Marker && len(payload) < PSMaxSize {
switch NALUType(payload) {
case NALUTypeSPS, NALUTypePPS:
buf = append(buf, payload...)
return nil
case NALUTypeSEI:
// RtspServer https://github.com/AlexxIT/go2rtc/issues/244
// sends, marked SPS, marked PPS, marked SEI, marked IFrame
return nil
}
}
if len(buf) == 0 {
// Amcrest IP4M-1051: 9, 7, 8, 6, 28...
// Amcrest IP4M-1051: 9, 6, 1
switch NALUType(payload) {
case NALUTypeIFrame:
// fix IFrame without SPS,PPS
buf = append(buf, ps...)
case NALUTypeSEI, NALUTypeAUD:
// fix ffmpeg with transcoding first frame
i := int(4 + binary.BigEndian.Uint32(payload))
// check if only one NAL (fix ffmpeg transcoding for Reolink RLC-510A)
if i == len(payload) {
return nil
}
payload = payload[i:]
if NALUType(payload) == NALUTypeIFrame {
for {
// Amcrest IP4M-1051: 9, 7, 8, 6, 28...
// Amcrest IP4M-1051: 9, 6, 1
switch NALUType(payload) {
case NALUTypeIFrame:
// fix IFrame without SPS,PPS
buf = append(buf, ps...)
case NALUTypeSEI, NALUTypeAUD:
// fix ffmpeg with transcoding first frame
i := int(4 + binary.BigEndian.Uint32(payload))
// check if only one NAL (fix ffmpeg transcoding for Reolink RLC-510A)
if i == len(payload) {
return nil
}
payload = payload[i:]
continue
}
break
}
}
@@ -70,7 +78,31 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
buf = buf[:0]
}
//log.Printf("[AVC] %v, len: %d", Types(payload), len(payload))
// should not be that huge SPS
if NALUType(payload) == NALUTypeSPS && binary.BigEndian.Uint32(payload) >= PSMaxSize {
// some Chinese buggy cameras has single packet with SPS+PPS+IFrame separated by 00 00 00 01
// https://github.com/AlexxIT/WebRTC/issues/391
// https://github.com/AlexxIT/WebRTC/issues/392
for i := 0; i < len(payload); {
if i+4 >= len(payload) {
break
}
size := bytes.Index(payload[i+4:], []byte{0, 0, 0, 1})
if size < 0 {
if i == 0 {
break
}
size = len(payload) - (i + 4)
}
binary.BigEndian.PutUint32(payload[i:], uint32(size))
i += size + 4
}
}
//log.Printf("[AVC] %v, len: %d, ts: %10d, seq: %d", Types(payload), len(payload), packet.Timestamp, packet.SequenceNumber)
clone := *packet
clone.Version = RTPPacketVersionAVC
+10 -8
View File
@@ -7,14 +7,16 @@ import (
)
const (
NALUTypePFrame = 1
NALUTypeIFrame = 19
NALUTypeIFrame2 = 20
NALUTypeIFrame3 = 21
NALUTypeVPS = 32
NALUTypeSPS = 33
NALUTypePPS = 34
NALUTypeFU = 49
NALUTypePFrame = 1
NALUTypeIFrame = 19
NALUTypeIFrame2 = 20
NALUTypeIFrame3 = 21
NALUTypeVPS = 32
NALUTypeSPS = 33
NALUTypePPS = 34
NALUTypePrefixSEI = 39
NALUTypeSuffixSEI = 40
NALUTypeFU = 49
)
func NALUType(b []byte) byte {
+10
View File
@@ -20,6 +20,16 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
nuType := (data[0] >> 1) & 0x3F
//log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", track.Codec.Name, nuType, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
// Fix for RtspServer https://github.com/AlexxIT/go2rtc/issues/244
if packet.Marker && len(data) < h264.PSMaxSize {
switch nuType {
case NALUTypeVPS, NALUTypeSPS, NALUTypePPS:
packet.Marker = false
case NALUTypePrefixSEI, NALUTypeSuffixSEI:
return nil
}
}
if nuType == NALUTypeFU {
switch data[2] >> 6 {
case 2: // begin
+18
View File
@@ -1,6 +1,7 @@
package homekit
import (
"encoding/json"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/hap"
@@ -11,6 +12,7 @@ import (
"github.com/brutella/hap/rtp"
"net"
"net/url"
"sync/atomic"
)
type Client struct {
@@ -263,3 +265,19 @@ func (c *Client) getMedias() []*streamer.Media {
return medias
}
func (c *Client) MarshalJSON() ([]byte, error) {
var recv uint32
for _, session := range c.sessions {
recv += atomic.LoadUint32(&session.Recv)
}
info := &streamer.Info{
Type: "HomeKit source",
URL: c.conn.URL(),
Medias: c.medias,
Tracks: c.tracks,
Recv: recv,
}
return json.Marshal(info)
}
+165
View File
@@ -0,0 +1,165 @@
package httpflv
import (
"encoding/binary"
"errors"
"math"
)
const (
TypeNumber byte = iota
TypeBoolean
TypeString
TypeObject
TypeEcmaArray = 8
TypeObjectEnd = 9
)
var Err = errors.New("amf0 read error")
// AMF0 spec: http://download.macromedia.com/pub/labs/amf/amf0_spec_121207.pdf
type AMF0 struct {
buf []byte
pos int
}
func NewReader(b []byte) *AMF0 {
return &AMF0{buf: b}
}
func (a *AMF0) ReadMetaData() map[string]interface{} {
if b, _ := a.ReadByte(); b != TypeString {
return nil
}
if s, _ := a.ReadString(); s != "onMetaData" {
return nil
}
b, _ := a.ReadByte()
switch b {
case TypeObject:
v, _ := a.ReadObject()
return v
case TypeEcmaArray:
v, _ := a.ReadEcmaArray()
return v
}
return nil
}
func (a *AMF0) ReadMap() (map[interface{}]interface{}, error) {
dict := make(map[interface{}]interface{})
for a.pos < len(a.buf) {
k, err := a.ReadItem()
if err != nil {
return nil, err
}
v, err := a.ReadItem()
if err != nil {
return nil, err
}
dict[k] = v
}
return dict, nil
}
func (a *AMF0) ReadItem() (interface{}, error) {
dataType, err := a.ReadByte()
if err != nil {
return nil, err
}
switch dataType {
case TypeNumber:
return a.ReadNumber()
case TypeBoolean:
v, err := a.ReadByte()
return v != 0, err
case TypeString:
return a.ReadString()
case TypeObject:
return a.ReadObject()
case TypeObjectEnd:
return nil, nil
}
return nil, Err
}
func (a *AMF0) ReadByte() (byte, error) {
if a.pos >= len(a.buf) {
return 0, Err
}
v := a.buf[a.pos]
a.pos++
return v, nil
}
func (a *AMF0) ReadNumber() (float64, error) {
if a.pos+8 >= len(a.buf) {
return 0, Err
}
v := binary.BigEndian.Uint64(a.buf[a.pos : a.pos+8])
a.pos += 8
return math.Float64frombits(v), nil
}
func (a *AMF0) ReadString() (string, error) {
if a.pos+2 >= len(a.buf) {
return "", Err
}
size := int(binary.BigEndian.Uint16(a.buf[a.pos:]))
a.pos += 2
if a.pos+size >= len(a.buf) {
return "", Err
}
s := string(a.buf[a.pos : a.pos+size])
a.pos += size
return s, nil
}
func (a *AMF0) ReadObject() (map[string]interface{}, error) {
obj := make(map[string]interface{})
for {
k, err := a.ReadString()
if err != nil {
return nil, err
}
v, err := a.ReadItem()
if err != nil {
return nil, err
}
if k == "" {
break
}
obj[k] = v
}
return obj, nil
}
func (a *AMF0) ReadEcmaArray() (map[string]interface{}, error) {
if a.pos+4 >= len(a.buf) {
return nil, Err
}
a.pos += 4 // skip size
return a.ReadObject()
}
+97
View File
@@ -0,0 +1,97 @@
package httpflv
import (
"fmt"
"github.com/deepch/vdk/format/flv/flvio"
"github.com/deepch/vdk/utils/bits/pio"
"io"
)
// TODO: rewrite all of this someday
func ReadTag(r io.Reader, b []byte) (tag flvio.Tag, ts int32, err error) {
if _, err = io.ReadFull(r, b[:flvio.TagHeaderLength]); err != nil {
return
}
var datalen int
if tag, ts, datalen, err = flvio.ParseTagHeader(b); err != nil {
return
}
data := make([]byte, datalen)
if _, err = io.ReadFull(r, data); err != nil {
return
}
n, err := ParseHeader(&tag, data)
if err != nil {
return
}
tag.Data = data[n:]
if _, err = io.ReadFull(r, b[:4]); err != nil {
return
}
return
}
func ParseHeader(self *flvio.Tag, b []byte) (n int, err error) {
switch self.Type {
case flvio.TAG_AUDIO:
return audioParseHeader(self, b)
case flvio.TAG_VIDEO:
return videoParseHeader(self, b)
}
return
}
func audioParseHeader(tag *flvio.Tag, b []byte) (n int, err error) {
if len(b) < n+1 {
err = fmt.Errorf("audiodata: parse invalid")
return
}
flags := b[n]
n++
tag.SoundFormat = flags >> 4
tag.SoundRate = (flags >> 2) & 0x3
tag.SoundSize = (flags >> 1) & 0x1
tag.SoundType = flags & 0x1
switch tag.SoundFormat {
case flvio.SOUND_AAC:
if len(b) < n+1 {
err = fmt.Errorf("audiodata: parse invalid")
return
}
tag.AACPacketType = b[n]
n++
}
return
}
func videoParseHeader(tag *flvio.Tag, b []byte) (n int, err error) {
if len(b) < n+1 {
err = fmt.Errorf("videodata: parse invalid")
return
}
flags := b[n]
tag.FrameType = flags >> 4
tag.CodecID = flags & 0xf
n++
if len(b) < n+4 {
err = fmt.Errorf("videodata: parse invalid")
return
}
tag.AVCPacketType = b[n]
n++
tag.CompositionTime = pio.I24BE(b[n:])
n += 3
return
}
+132 -22
View File
@@ -2,8 +2,9 @@ package httpflv
import (
"bufio"
"errors"
"bytes"
"github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/aacparser"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/flv/flvio"
"github.com/deepch/vdk/utils/bits/pio"
@@ -41,8 +42,12 @@ func Accept(res *http.Response) (*Conn, error) {
return nil, err
}
if flags&flvio.FILE_HAS_VIDEO == 0 {
return nil, errors.New("not supported")
if flags&flvio.FILE_HAS_VIDEO != 0 {
c.videoIdx = -1
}
if flags&flvio.FILE_HAS_AUDIO != 0 {
c.audioIdx = -1
}
if _, err = c.reader.Discard(n); err != nil {
@@ -56,49 +61,154 @@ type Conn struct {
conn io.ReadCloser
reader *bufio.Reader
buf []byte
videoIdx int8
audioIdx int8
}
func (c *Conn) Streams() ([]av.CodecData, error) {
for {
var video, audio av.CodecData
// Normal software sends:
// 1. Video/audio flag in header
// 2. MetaData as first tag (with video/audio codec info)
// 3. Video/audio headers in 2nd and 3rd tag
// Reolink camera sends:
// 1. Empty video/audio flag
// 2. MedaData without stereo key for AAC
// 3. Audio header after Video keyframe tag
waitVideo := c.videoIdx != 0
waitAudio := c.audioIdx != 0
for i := 0; i < 20; i++ {
tag, _, err := flvio.ReadTag(c.reader, c.buf)
if err != nil {
return nil, err
}
if tag.Type != flvio.TAG_VIDEO || tag.AVCPacketType != flvio.AAC_SEQHDR {
continue
//log.Printf("[FLV] type=%d avc=%d aac=%d video=%t audio=%t", tag.Type, tag.AVCPacketType, tag.AACPacketType, video != nil, audio != nil)
switch tag.Type {
case flvio.TAG_SCRIPTDATA:
if meta := NewReader(tag.Data).ReadMetaData(); meta != nil {
waitVideo = meta["videocodecid"] != nil
// don't wait audio tag because parse all info from MetaData
waitAudio = false
audio = parseAudioConfig(meta)
} else {
waitVideo = bytes.Contains(tag.Data, []byte("videocodecid"))
waitAudio = bytes.Contains(tag.Data, []byte("audiocodecid"))
}
case flvio.TAG_VIDEO:
if tag.AVCPacketType == flvio.AVC_SEQHDR {
video, _ = h264parser.NewCodecDataFromAVCDecoderConfRecord(tag.Data)
}
waitVideo = false
case flvio.TAG_AUDIO:
if tag.SoundFormat == flvio.SOUND_AAC && tag.AACPacketType == flvio.AAC_SEQHDR {
audio, _ = aacparser.NewCodecDataFromMPEG4AudioConfigBytes(tag.Data)
}
waitAudio = false
}
stream, err := h264parser.NewCodecDataFromAVCDecoderConfRecord(tag.Data)
if err != nil {
return nil, err
if !waitVideo && !waitAudio {
break
}
return []av.CodecData{stream}, nil
}
if video != nil && audio != nil {
c.videoIdx = 0
c.audioIdx = 1
return []av.CodecData{video, audio}, nil
} else if video != nil {
c.videoIdx = 0
c.audioIdx = -1
return []av.CodecData{video}, nil
} else if audio != nil {
c.videoIdx = -1
c.audioIdx = 0
return []av.CodecData{audio}, nil
}
return nil, nil
}
func (c *Conn) ReadPacket() (av.Packet, error) {
for {
tag, ts, err := flvio.ReadTag(c.reader, c.buf)
tag, ts, err := ReadTag(c.reader, c.buf)
if err != nil {
return av.Packet{}, err
}
if tag.Type != flvio.TAG_VIDEO || tag.AVCPacketType != flvio.AVC_NALU {
continue
}
switch tag.Type {
case flvio.TAG_VIDEO:
if c.videoIdx < 0 || tag.AVCPacketType != flvio.AVC_NALU {
continue
}
return av.Packet{
Idx: 0,
Data: tag.Data,
CompositionTime: flvio.TsToTime(tag.CompositionTime),
IsKeyFrame: tag.FrameType == flvio.FRAME_KEY,
Time: flvio.TsToTime(ts),
}, nil
//log.Printf("[FLV] %v, len: %d, ts: %10d", h264.Types(tag.Data), len(tag.Data), flvio.TsToTime(ts))
return av.Packet{
Idx: c.videoIdx,
Data: tag.Data,
CompositionTime: flvio.TsToTime(tag.CompositionTime),
IsKeyFrame: tag.FrameType == flvio.FRAME_KEY,
Time: flvio.TsToTime(ts),
}, nil
case flvio.TAG_AUDIO:
if c.audioIdx < 0 || tag.SoundFormat != flvio.SOUND_AAC || tag.AACPacketType != flvio.AAC_RAW {
continue
}
return av.Packet{Idx: c.audioIdx, Data: tag.Data, Time: flvio.TsToTime(ts)}, nil
}
}
}
func (c *Conn) Close() (err error) {
return c.conn.Close()
}
func parseAudioConfig(meta map[string]interface{}) av.CodecData {
if meta["audiocodecid"] != float64(10) {
return nil
}
config := aacparser.MPEG4AudioConfig{
ObjectType: aacparser.AOT_AAC_LC,
}
switch v := meta["audiosamplerate"].(type) {
case float64:
config.SampleRate = int(v)
default:
return nil
}
switch meta["stereo"] {
case true:
config.ChannelConfig = 2
config.ChannelLayout = av.CH_STEREO
default:
// Reolink doesn't have this setting
config.ChannelConfig = 1
config.ChannelLayout = av.CH_MONO
}
buf := &bytes.Buffer{}
if err := aacparser.WriteMPEG4AudioConfig(buf, config); err != nil {
return nil
}
return aacparser.CodecData{
Config: config,
ConfigBytes: buf.Bytes(),
}
}
+318
View File
@@ -0,0 +1,318 @@
package iso
const (
Ftyp = "ftyp"
Moov = "moov"
MoovMvhd = "mvhd"
MoovTrak = "trak"
MoovTrakTkhd = "tkhd"
MoovTrakMdia = "mdia"
MoovTrakMdiaMdhd = "mdhd"
MoovTrakMdiaHdlr = "hdlr"
MoovTrakMdiaMinf = "minf"
MoovTrakMdiaMinfVmhd = "vmhd"
MoovTrakMdiaMinfSmhd = "smhd"
MoovTrakMdiaMinfDinf = "dinf"
MoovTrakMdiaMinfDinfDref = "dref"
MoovTrakMdiaMinfDinfDrefUrl = "url "
MoovTrakMdiaMinfStbl = "stbl"
MoovTrakMdiaMinfStblStsd = "stsd"
MoovTrakMdiaMinfStblStts = "stts"
MoovTrakMdiaMinfStblStsc = "stsc"
MoovTrakMdiaMinfStblStsz = "stsz"
MoovTrakMdiaMinfStblStco = "stco"
MoovMvex = "mvex"
MoovMvexTrex = "trex"
Moof = "moof"
MoofMfhd = "mfhd"
MoofTraf = "traf"
MoofTrafTfhd = "tfhd"
MoofTrafTfdt = "tfdt"
MoofTrafTrun = "trun"
Mdat = "mdat"
)
func (m *Movie) WriteFileType() {
m.StartAtom(Ftyp)
m.WriteString("iso5")
m.WriteUint32(512)
m.WriteString("iso5")
m.WriteString("iso6")
m.WriteString("mp41")
m.EndAtom()
}
func (m *Movie) WriteMovieHeader() {
m.StartAtom(MoovMvhd)
m.Skip(1) // version
m.Skip(3) // flags
m.Skip(4) // create time
m.Skip(4) // modify time
m.WriteUint32(1000) // time scale
m.Skip(4) // duration
m.WriteFloat32(1) // preferred rate
m.WriteFloat16(1) // preferred volume
m.Skip(10) // reserved
m.WriteMatrix()
m.Skip(6 * 4) // predefined?
m.WriteUint32(0xFFFFFFFF) // next track ID
m.EndAtom()
}
func (m *Movie) WriteTrackHeader(id uint32, width, height uint16) {
const (
TkhdTrackEnabled = 0x0001
TkhdTrackInMovie = 0x0002
TkhdTrackInPreview = 0x0004
TkhdTrackInPoster = 0x0008
)
// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-32963
m.StartAtom(MoovTrakTkhd)
m.Skip(1) // version
m.WriteUint24(TkhdTrackEnabled | TkhdTrackInMovie)
m.Skip(4) // create time
m.Skip(4) // modify time
m.WriteUint32(id) // trackID
m.Skip(4) // reserved
m.Skip(4) // duration
m.Skip(8) // reserved
m.Skip(2) // layer
if width > 0 {
m.Skip(2)
m.Skip(2)
} else {
m.WriteUint16(1) // alternate group
m.WriteFloat16(1) // volume
}
m.Skip(2) // reserved
m.WriteMatrix()
if width > 0 {
m.WriteFloat32(float64(width))
m.WriteFloat32(float64(height))
} else {
m.Skip(4)
m.Skip(4)
}
m.EndAtom()
}
func (m *Movie) WriteMediaHeader(timescale uint32) {
// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-32999
m.StartAtom(MoovTrakMdiaMdhd)
m.Skip(1) // version
m.Skip(3) // flags
m.Skip(4) // creation time
m.Skip(4) // modification time
m.WriteUint32(timescale) // timescale
m.Skip(4) // duration
m.WriteUint16(0x55C4) // language (Unspecified)
m.Skip(2) // quality
m.EndAtom()
}
func (m *Movie) WriteMediaHandler(s, name string) {
// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-33004
m.StartAtom(MoovTrakMdiaHdlr)
m.Skip(1) // version
m.Skip(3) // flags
m.Skip(4)
m.WriteString(s) // handler type (4 byte!)
m.Skip(3 * 4) // reserved
m.WriteString(name) // handler name (any len)
m.Skip(1) // end string
m.EndAtom()
}
func (m *Movie) WriteVideoMediaInfo() {
// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-33012
m.StartAtom(MoovTrakMdiaMinfVmhd)
m.Skip(1) // version
m.WriteUint24(1) // flags (You should always set this flag to 1)
m.Skip(2) // graphics mode
m.Skip(3 * 2) // op color
m.EndAtom()
}
func (m *Movie) WriteAudioMediaInfo() {
m.StartAtom(MoovTrakMdiaMinfSmhd)
m.Skip(1) // version
m.Skip(3) // flags
m.Skip(4) // balance
m.EndAtom()
}
func (m *Movie) WriteDataInfo() {
// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25680
m.StartAtom(MoovTrakMdiaMinfDinf)
m.StartAtom(MoovTrakMdiaMinfDinfDref)
m.Skip(1) // version
m.Skip(3) // flags
m.WriteUint32(1) // childrens
m.StartAtom(MoovTrakMdiaMinfDinfDrefUrl)
m.Skip(1) // version
m.WriteUint24(1) // flags (self reference)
m.EndAtom()
m.EndAtom() // DREF
m.EndAtom() // DINF
}
func (m *Movie) WriteSampleTable(writeSampleDesc func()) {
// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-33040
m.StartAtom(MoovTrakMdiaMinfStbl)
m.StartAtom(MoovTrakMdiaMinfStblStsd)
m.Skip(1) // version
m.Skip(3) // flags
m.WriteUint32(1) // entry count
writeSampleDesc()
m.EndAtom()
m.StartAtom(MoovTrakMdiaMinfStblStts)
m.Skip(1) // version
m.Skip(3) // flags
m.Skip(4) // entry count
m.EndAtom()
m.StartAtom(MoovTrakMdiaMinfStblStsc)
m.Skip(1) // version
m.Skip(3) // flags
m.Skip(4) // entry count
m.EndAtom()
m.StartAtom(MoovTrakMdiaMinfStblStsz)
m.Skip(1) // version
m.Skip(3) // flags
m.Skip(4) // sample size
m.Skip(4) // entry count
m.EndAtom()
m.StartAtom(MoovTrakMdiaMinfStblStco)
m.Skip(1) // version
m.Skip(3) // flags
m.Skip(4) // entry count
m.EndAtom()
m.EndAtom()
}
func (m *Movie) WriteTrackExtend(id uint32) {
m.StartAtom(MoovMvexTrex)
m.Skip(1) // version
m.Skip(3) // flags
m.WriteUint32(id) // trackID
m.WriteUint32(1) // default sample description index
m.Skip(4) // default sample duration
m.Skip(4) // default sample size
m.Skip(4) // default sample flags
m.EndAtom()
}
func (m *Movie) WriteVideoTrack(id uint32, codec string, timescale uint32, width, height uint16, conf []byte) {
m.StartAtom(MoovTrak)
m.WriteTrackHeader(id, width, height)
m.StartAtom(MoovTrakMdia)
m.WriteMediaHeader(timescale)
m.WriteMediaHandler("vide", "VideoHandler")
m.StartAtom(MoovTrakMdiaMinf)
m.WriteVideoMediaInfo()
m.WriteDataInfo()
m.WriteSampleTable(func() {
m.WriteVideo(codec, width, height, conf)
})
m.EndAtom() // MINF
m.EndAtom() // MDIA
m.EndAtom() // TRAK
}
func (m *Movie) WriteAudioTrack(id uint32, codec string, timescale uint32, channels uint16, conf []byte) {
m.StartAtom(MoovTrak)
m.WriteTrackHeader(id, 0, 0)
m.StartAtom(MoovTrakMdia)
m.WriteMediaHeader(timescale)
m.WriteMediaHandler("soun", "SoundHandler")
m.StartAtom(MoovTrakMdiaMinf)
m.WriteAudioMediaInfo()
m.WriteDataInfo()
m.WriteSampleTable(func() {
m.WriteAudio(codec, channels, timescale, conf)
})
m.EndAtom() // MINF
m.EndAtom() // MDIA
m.EndAtom() // TRAK
}
func (m *Movie) WriteMovieFragment(seq, tid, duration, size uint32, time uint64) {
m.StartAtom(Moof)
m.StartAtom(MoofMfhd)
m.Skip(1) // version
m.Skip(3) // flags
m.WriteUint32(seq) // sequence number
m.EndAtom()
m.StartAtom(MoofTraf)
const (
TfhdDefaultSampleDuration = 0x000008
TfhdDefaultSampleSize = 0x000010
TfhdDefaultSampleFlags = 0x000020
TfhdDefaultBaseIsMoof = 0x020000
)
m.StartAtom(MoofTrafTfhd)
m.Skip(1) // version
m.WriteUint24(
TfhdDefaultSampleDuration |
TfhdDefaultSampleSize |
TfhdDefaultSampleFlags |
TfhdDefaultBaseIsMoof,
)
m.WriteUint32(tid) // track id
m.WriteUint32(duration) // default sample duration
m.WriteUint32(size) // default sample size
m.WriteUint32(0x2000000) // default sample flags
m.EndAtom()
m.StartAtom(MoofTrafTfdt)
m.WriteBytes(1) // version
m.Skip(3) // flags
m.WriteUint64(time) // base media decode time
m.EndAtom()
const (
TrunDataOffset = 0x000001
TrunFirstSampleFlags = 0x000004
TrunSampleDuration = 0x0000100
TrunSampleSize = 0x0000200
TrunSampleFlags = 0x0000400
TrunSampleCTS = 0x0000800
)
m.StartAtom(MoofTrafTrun)
m.Skip(1) // version
m.WriteUint24(TrunDataOffset) // flags
m.WriteUint32(1) // sample count
// data offset: current pos + uint32 len + MDAT header len
m.WriteUint32(uint32(len(m.b)) + 4 + 8)
m.EndAtom() // TRUN
m.EndAtom() // TRAF
m.EndAtom() // MOOF
}
func (m *Movie) WriteData(b []byte) {
m.StartAtom(Mdat)
m.Write(b)
m.EndAtom()
}
+151
View File
@@ -0,0 +1,151 @@
package iso
import "github.com/AlexxIT/go2rtc/pkg/streamer"
func (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) {
// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html
switch codec {
case streamer.CodecH264:
m.StartAtom("avc1")
case streamer.CodecH265:
m.StartAtom("hev1")
default:
panic("unsupported iso video: " + codec)
}
m.Skip(6)
m.WriteUint16(1) // data_reference_index
m.Skip(2) // version
m.Skip(2) // revision
m.Skip(4) // vendor
m.Skip(4) // temporal quality
m.Skip(4) // spatial quality
m.WriteUint16(width) // width
m.WriteUint16(height) // height
m.WriteFloat32(72) // horizontal resolution
m.WriteFloat32(72) // vertical resolution
m.Skip(4) // reserved
m.WriteUint16(1) // frame count
m.Skip(32) // compressor name
m.WriteUint16(24) // depth
m.WriteUint16(0xFFFF) // color table id (-1)
switch codec {
case streamer.CodecH264:
m.StartAtom("avcC")
case streamer.CodecH265:
m.StartAtom("hvcC")
}
m.Write(conf)
m.EndAtom() // AVCC
m.EndAtom() // AVC1
}
func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, conf []byte) {
switch codec {
case streamer.CodecAAC, streamer.CodecMP3:
m.StartAtom("mp4a")
case streamer.CodecOpus:
m.StartAtom("Opus")
case streamer.CodecPCMU:
m.StartAtom("ulaw")
case streamer.CodecPCMA:
m.StartAtom("alaw")
default:
panic("unsupported iso audio: " + codec)
}
m.Skip(6)
m.WriteUint16(1) // data_reference_index
m.Skip(2) // version
m.Skip(2) // revision
m.Skip(4) // vendor
m.WriteUint16(channels) // channel_count
m.WriteUint16(16) // sample_size
m.Skip(2) // compression id
m.Skip(2) // reserved
m.WriteFloat32(float64(sampleRate)) // sample_rate
switch codec {
case streamer.CodecAAC:
m.WriteEsdsAAC(conf)
case streamer.CodecMP3:
m.WriteEsdsMP3()
case streamer.CodecOpus:
// don't know what means this magic
m.StartAtom("dOps")
m.WriteBytes(0, 0x02, 0x01, 0x38, 0, 0, 0xBB, 0x80, 0, 0, 0)
m.EndAtom()
case streamer.CodecPCMU, streamer.CodecPCMA:
// don't know what means this magic
m.StartAtom("chan")
m.WriteBytes(0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0)
m.EndAtom()
}
m.EndAtom() // MP4A/OPUS
}
func (m *Movie) WriteEsdsAAC(conf []byte) {
m.StartAtom("esds")
m.Skip(1) // version
m.Skip(3) // flags
// MP4ESDescrTag[3]:
// - MP4DecConfigDescrTag[4]:
// - MP4DecSpecificDescrTag[5]: conf
// - Other[6]
const header = 5
const size3 = 3
const size4 = 13
size5 := byte(len(conf))
const size6 = 1
m.WriteBytes(3, 0x80, 0x80, 0x80, size3+header+size4+header+size5+header+size6)
m.Skip(2) // es id
m.Skip(1) // es flags
m.WriteBytes(4, 0x80, 0x80, 0x80, size4+header+size5)
m.WriteBytes(0x40) // object id
m.WriteBytes(0x15) // stream type
m.Skip(3) // buffer size db
m.Skip(4) // max bitraga
m.Skip(4) // avg bitraga
m.WriteBytes(5, 0x80, 0x80, 0x80, size5)
m.Write(conf)
m.WriteBytes(6, 0x80, 0x80, 0x80, 1)
m.WriteBytes(2) // ?
m.EndAtom() // ESDS
}
func (m *Movie) WriteEsdsMP3() {
m.StartAtom("esds")
m.Skip(1) // version
m.Skip(3) // flags
// MP4ESDescrTag[3]:
// - MP4DecConfigDescrTag[4]:
// - Other[6]
const header = 5
const size3 = 3
const size4 = 13
const size6 = 1
m.WriteBytes(3, 0x80, 0x80, 0x80, size3+header+size4+header+size6)
m.Skip(2) // es id
m.Skip(1) // es flags
m.WriteBytes(4, 0x80, 0x80, 0x80, size4)
m.WriteBytes(0x6B) // object id
m.WriteBytes(0x15) // stream type
m.Skip(3) // buffer size db
m.Skip(4) // max bitraga
m.Skip(4) // avg bitraga
m.WriteBytes(6, 0x80, 0x80, 0x80, 1)
m.WriteBytes(2) // ?
m.EndAtom() // ESDS
}
+91
View File
@@ -0,0 +1,91 @@
package iso
import (
"encoding/binary"
"math"
)
type Movie struct {
b []byte
start []int
}
func NewMovie(size int) *Movie {
return &Movie{b: make([]byte, 0, size)}
}
func (m *Movie) Bytes() []byte {
return m.b
}
func (m *Movie) StartAtom(name string) {
m.start = append(m.start, len(m.b))
m.b = append(m.b, 0, 0, 0, 0)
m.b = append(m.b, name...)
}
func (m *Movie) EndAtom() {
n := len(m.start) - 1
i := m.start[n]
size := uint32(len(m.b) - i)
binary.BigEndian.PutUint32(m.b[i:], size)
m.start = m.start[:n]
}
func (m *Movie) Write(b []byte) {
m.b = append(m.b, b...)
}
func (m *Movie) WriteBytes(b ...byte) {
m.b = append(m.b, b...)
}
func (m *Movie) WriteString(s string) {
m.b = append(m.b, s...)
}
func (m *Movie) Skip(n int) {
m.b = append(m.b, make([]byte, n)...)
}
func (m *Movie) WriteUint16(v uint16) {
m.b = append(m.b, byte(v>>8), byte(v))
}
func (m *Movie) WriteUint24(v uint32) {
m.b = append(m.b, byte(v>>16), byte(v>>8), byte(v))
}
func (m *Movie) WriteUint32(v uint32) {
m.b = append(m.b, byte(v>>24), byte(v>>16), byte(v>>8), byte(v))
}
func (m *Movie) WriteUint64(v uint64) {
m.b = append(m.b, byte(v>>56), byte(v>>48), byte(v>>40), byte(v>>32), byte(v>>24), byte(v>>16), byte(v>>8), byte(v))
}
func (m *Movie) WriteFloat16(f float64) {
i, f := math.Modf(f)
f *= 256
m.b = append(m.b, byte(i), byte(f))
}
func (m *Movie) WriteFloat32(f float64) {
i, f := math.Modf(f)
f *= 65536
m.b = append(m.b, byte(uint16(i)>>8), byte(i), byte(uint16(f)>>8), byte(f))
}
func (m *Movie) WriteMatrix() {
m.WriteUint32(0x00010000)
m.Skip(4)
m.Skip(4)
m.Skip(4)
m.WriteUint32(0x00010000)
m.Skip(4)
m.Skip(4)
m.Skip(4)
m.WriteUint32(0x40000000)
}
+52 -12
View File
@@ -14,9 +14,19 @@ import (
"io"
"net/http"
"strings"
"sync"
"sync/atomic"
"time"
)
type State byte
const (
StateNone State = iota
StateConn
StateHandle
)
type Client struct {
streamer.Element
@@ -26,12 +36,14 @@ type Client struct {
medias []*streamer.Media
tracks map[byte]*streamer.Track
closed bool
msg *message
t0 time.Time
buffer chan []byte
state State
mu sync.Mutex
recv uint32
}
func NewClient(id string) *Client {
@@ -69,16 +81,26 @@ func (c *Client) Dial() (err error) {
return err
}
c.state = StateConn
return nil
}
func (c *Client) Handle() error {
c.buffer = make(chan []byte, 5)
// add delay to the stream for smooth playing (not a best solution)
c.t0 = time.Now().Add(time.Second)
// processing stream in separate thread for lower delay between packets
go c.worker()
c.mu.Lock()
if c.state == StateConn {
c.buffer = make(chan []byte, 5)
c.state = StateHandle
// processing stream in separate thread for lower delay between packets
go c.worker(c.buffer)
}
c.mu.Unlock()
_, data, err := c.conn.ReadMessage()
if err != nil {
@@ -87,7 +109,12 @@ func (c *Client) Handle() error {
track := c.tracks[c.msg.Track]
if track != nil {
c.buffer <- data
c.mu.Lock()
if c.state == StateHandle {
c.buffer <- data
atomic.AddUint32(&c.recv, uint32(len(data)))
}
c.mu.Unlock()
}
// we have one unprocessed msg after getTracks
@@ -114,7 +141,12 @@ func (c *Client) Handle() error {
track = c.tracks[msg.Track]
if track != nil {
c.buffer <- data
c.mu.Lock()
if c.state == StateHandle {
c.buffer <- data
atomic.AddUint32(&c.recv, uint32(len(data)))
}
c.mu.Unlock()
}
default:
@@ -124,11 +156,19 @@ func (c *Client) Handle() error {
}
func (c *Client) Close() error {
if c.conn == nil {
c.mu.Lock()
defer c.mu.Unlock()
switch c.state {
case StateNone:
return nil
case StateConn:
case StateHandle:
close(c.buffer)
}
close(c.buffer)
c.closed = true
c.state = StateNone
return c.conn.Close()
}
@@ -208,13 +248,13 @@ func (c *Client) getTracks() error {
}
}
func (c *Client) worker() {
func (c *Client) worker(buffer chan []byte) {
var track *streamer.Track
for _, track = range c.tracks {
break
}
for data := range c.buffer {
for data := range buffer {
moof := &fmp4io.MovieFrag{}
if _, err := moof.Unmarshal(data, 0); err != nil {
continue
+19 -1
View File
@@ -1,8 +1,10 @@
package ivideon
import (
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"sync/atomic"
)
func (c *Client) GetMedias() []*streamer.Media {
@@ -20,7 +22,7 @@ func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streame
func (c *Client) Start() error {
err := c.Handle()
if c.closed {
if c.buffer == nil {
return nil
}
return err
@@ -29,3 +31,19 @@ func (c *Client) Start() error {
func (c *Client) Stop() error {
return c.Close()
}
func (c *Client) MarshalJSON() ([]byte, error) {
var tracks []*streamer.Track
for _, track := range c.tracks {
tracks = append(tracks, track)
}
info := &streamer.Info{
Type: "Ivideon source",
URL: c.ID,
Medias: c.medias,
Tracks: tracks,
Recv: atomic.LoadUint32(&c.recv),
}
return json.Marshal(info)
}
+27 -1
View File
@@ -2,6 +2,7 @@ package mjpeg
import (
"bufio"
"encoding/json"
"errors"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/tcp"
@@ -11,6 +12,7 @@ import (
"net/textproto"
"strconv"
"strings"
"sync/atomic"
"time"
)
@@ -24,6 +26,7 @@ type Client struct {
res *http.Response
track *streamer.Track
recv uint32
}
func NewClient(res *http.Response) *Client {
@@ -64,10 +67,23 @@ func (c *Client) Start() error {
}
func (c *Client) Stop() error {
// important for close reader/writer gorutines
_ = c.res.Body.Close()
c.closed = true
return nil
}
func (c *Client) MarshalJSON() ([]byte, error) {
info := &streamer.Info{
Type: "MJPEG source",
URL: c.res.Request.URL.String(),
RemoteAddr: c.RemoteAddr,
UserAgent: c.UserAgent,
Recv: atomic.LoadUint32(&c.recv),
}
return json.Marshal(info)
}
func (c *Client) startJPEG() error {
buf, err := io.ReadAll(c.res.Body)
if err != nil {
@@ -77,6 +93,8 @@ func (c *Client) startJPEG() error {
packet := &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
_ = c.track.WriteRTP(packet)
atomic.AddUint32(&c.recv, uint32(len(buf)))
req := c.res.Request
for !c.closed {
@@ -96,13 +114,19 @@ func (c *Client) startJPEG() error {
packet = &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
_ = c.track.WriteRTP(packet)
atomic.AddUint32(&c.recv, uint32(len(buf)))
}
return nil
}
func (c *Client) startMJPEG(boundary string) error {
boundary = "--" + boundary
// some cameras add prefix to boundary header:
// https://github.com/TheTimeWalker/wallpanel-android
if !strings.HasPrefix(boundary, "--") {
boundary = "--" + boundary
}
r := bufio.NewReader(c.res.Body)
tp := textproto.NewReader(r)
@@ -139,6 +163,8 @@ func (c *Client) startMJPEG(boundary string) error {
packet := &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
_ = c.track.WriteRTP(packet)
atomic.AddUint32(&c.recv, uint32(len(buf)))
if _, err = r.Discard(2); err != nil {
return err
}
+14 -1
View File
@@ -1,8 +1,10 @@
package mjpeg
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
"sync/atomic"
)
type Consumer struct {
@@ -14,7 +16,7 @@ type Consumer struct {
codecs []*streamer.Codec
start bool
send int
send uint32
}
func (c *Consumer) GetMedias() []*streamer.Media {
@@ -28,6 +30,7 @@ func (c *Consumer) GetMedias() []*streamer.Media {
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
push := func(packet *rtp.Packet) error {
c.Fire(packet.Payload)
atomic.AddUint32(&c.send, uint32(len(packet.Payload)))
return nil
}
@@ -38,3 +41,13 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
return track.Bind(push)
}
func (c *Consumer) MarshalJSON() ([]byte, error) {
info := &streamer.Info{
Type: "MJPEG client",
RemoteAddr: c.RemoteAddr,
UserAgent: c.UserAgent,
Send: atomic.LoadUint32(&c.send),
}
return json.Marshal(info)
}
+2 -2
View File
@@ -138,9 +138,9 @@ var chm_ac_symbols = []byte{
0xf9, 0xfa,
}
func MakeHeaders(t byte, w, h uint16, lqt, cqt []byte) []byte {
func MakeHeaders(p []byte, t byte, w, h uint16, lqt, cqt []byte) []byte {
// Appendix A from https://www.rfc-editor.org/rfc/rfc2435
p := []byte{0xFF, 0xD8}
p = append(p, 0xFF, 0xD8)
p = MakeQuantHeader(p, lqt, 0)
p = MakeQuantHeader(p, cqt, 1)
+26 -13
View File
@@ -6,7 +6,7 @@ import (
)
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
var header, payload []byte
buf := make([]byte, 0, 512*1024) // 512K
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
@@ -25,7 +25,7 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
b = b[8:]
}
if header == nil {
if len(buf) == 0 {
var lqt, cqt []byte
// 3.1.8. Quantization Table header
@@ -43,32 +43,41 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
w := uint16(packet.Payload[6]) << 3
h := uint16(packet.Payload[7]) << 3
// fix 2560x1920 and 2560x1440
if w == 512 && (h == 1920 || h == 1440) {
// fix sizes more than 2040
switch {
// 512x1920 512x1440
case w == cutSize(2560) && (h == 1920 || h == 1440):
w = 2560
// 1792x112
case w == cutSize(3840) && h == cutSize(2160):
w = 3840
h = 2160
// 256x1296
case w == cutSize(2304) && h == 1296:
w = 2304
}
//fmt.Printf("t: %d, q: %d, w: %d, h: %d\n", t, q, w, h)
header = MakeHeaders(t, w, h, lqt, cqt)
buf = MakeHeaders(buf, t, w, h, lqt, cqt)
}
// 3.1.9. JPEG Payload
payload = append(payload, b...)
buf = append(buf, b...)
if !packet.Marker {
return nil
}
b = append(header, payload...)
if end := b[len(b)-2:]; end[0] != 0xFF && end[1] != 0xD9 {
b = append(b, 0xFF, 0xD9)
if end := buf[len(buf)-2:]; end[0] != 0xFF && end[1] != 0xD9 {
buf = append(buf, 0xFF, 0xD9)
}
header = nil
payload = nil
clone := *packet
clone.Payload = buf
packet.Payload = b
return push(packet)
buf = buf[:0] // clear buffer
return push(&clone)
}
}
}
@@ -81,6 +90,10 @@ func RTPPay() streamer.WrapperFunc {
}
}
func cutSize(size uint16) uint16 {
return ((size >> 3) & 0xFF) << 3
}
//func RTPPay() streamer.WrapperFunc {
// const packetSize = 1436
//
+25 -14
View File
@@ -1,19 +1,30 @@
## Fragmented MP4
```
ffmpeg -i "rtsp://..." -movflags +frag_keyframe+separate_moof+default_base_moof+empty_moov -frag_duration 1 -c copy -t 5 sample.mp4
```
- movflags frag_keyframe
Start a new fragment at each video keyframe.
- frag_duration duration
Create fragments that are duration microseconds long.
- movflags separate_moof
Write a separate moof (movie fragment) atom for each track.
- movflags default_base_moof
Similarly to the omit_tfhd_offset, this flag avoids writing the absolute base_data_offset field in tfhd atoms, but does so by using the new default-base-is-moof flag instead.
https://ffmpeg.org/ffmpeg-formats.html#Options-13
## HEVC
Browser | avc1 | hvc1 | hev1
------------|------|------|---
Mac Chrome | + | - | +
Mac Safari | + | + | -
iOS 15? | + | + | -
Mac Firefox | + | - | -
iOS 12 | + | - | -
Android 13 | + | - | -
```
ffmpeg -i input-hev1.mp4 -c:v copy -tag:v hvc1 -c:a copy output-hvc1.mp4
Stream #0:0(eng): Video: hevc (Main) (hev1 / 0x31766568), yuv420p(tv, progressive), 720x404, 164 kb/s, 29.97 fps,
Stream #0:0(eng): Video: hevc (Main) (hvc1 / 0x31637668), yuv420p(tv, progressive), 720x404, 164 kb/s, 29.97 fps,
```
| Browser | avc1 | hvc1 | hev1 |
|-------------|------|------|------|
| Mac Chrome | + | - | + |
| Mac Safari | + | + | - |
| iOS 15? | + | + | - |
| Mac Firefox | + | - | - |
| iOS 12 | + | - | - |
| Android 13 | + | - | - |
## Useful links
+41 -12
View File
@@ -7,6 +7,7 @@ import (
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
"sync/atomic"
)
type Consumer struct {
@@ -20,7 +21,17 @@ type Consumer struct {
codecs []*streamer.Codec
wait byte
send int
send uint32
}
// ParseQuery - like usual parse, but with mp4 param handler
func ParseQuery(query map[string][]string) []*streamer.Media {
if query["mp4"] != nil {
cons := Consumer{}
return cons.GetMedias()
}
return streamer.ParseQuery(query)
}
const (
@@ -76,7 +87,7 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
}
buf := c.muxer.Marshal(trackID, packet)
c.send += len(buf)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
return nil
@@ -108,7 +119,7 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
}
buf := c.muxer.Marshal(trackID, packet)
c.send += len(buf)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
return nil
@@ -128,7 +139,7 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
}
buf := c.muxer.Marshal(trackID, packet)
c.send += len(buf)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
return nil
@@ -139,14 +150,33 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
push = wrapper(push)
}
return track.Bind(push)
case streamer.CodecOpus, streamer.CodecMP3, streamer.CodecPCMU, streamer.CodecPCMA:
push := func(packet *rtp.Packet) error {
if c.wait != waitNone {
return nil
}
buf := c.muxer.Marshal(trackID, packet)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
return nil
}
return track.Bind(push)
}
panic("unsupported codec")
}
func (c *Consumer) MimeCodecs() string {
return c.muxer.MimeCodecs(c.codecs)
}
func (c *Consumer) MimeType() string {
return c.muxer.MimeType(c.codecs)
return `video/mp4; codecs="` + c.MimeCodecs() + `"`
}
func (c *Consumer) Init() ([]byte, error) {
@@ -163,12 +193,11 @@ func (c *Consumer) Start() {
//
func (c *Consumer) MarshalJSON() ([]byte, error) {
v := map[string]interface{}{
"type": "MP4 server consumer",
"send": c.send,
"remote_addr": c.RemoteAddr,
"user_agent": c.UserAgent,
info := &streamer.Info{
Type: "MP4 client",
RemoteAddr: c.RemoteAddr,
UserAgent: c.UserAgent,
Send: atomic.LoadUint32(&c.send),
}
return json.Marshal(v)
return json.Marshal(info)
}
+65 -156
View File
@@ -1,17 +1,13 @@
package mp4
import (
"encoding/binary"
"encoding/hex"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/iso"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/codec/h265parser"
"github.com/deepch/vdk/format/fmp4/fmp4io"
"github.com/deepch/vdk/format/mp4/mp4io"
"github.com/deepch/vdk/format/mp4f/mp4fio"
"github.com/pion/rtp"
)
@@ -21,8 +17,15 @@ type Muxer struct {
pts []uint32
}
func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
s := `video/mp4; codecs="`
const (
MimeH264 = "avc1.640029"
MimeH265 = "hvc1.1.6.L153.B0"
MimeAAC = "mp4a.40.2"
MimeOpus = "opus"
)
func (m *Muxer) MimeCodecs(codecs []*streamer.Codec) string {
var s string
for i, codec := range codecs {
if i > 0 {
@@ -35,17 +38,23 @@ func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
case streamer.CodecH265:
// H.265 profile=main level=5.1
// hvc1 - supported in Safari, hev1 - doesn't, both supported in Chrome
s += "hvc1.1.6.L153.B0"
s += MimeH265
case streamer.CodecAAC:
s += "mp4a.40.2"
s += MimeAAC
case streamer.CodecOpus:
s += MimeOpus
}
}
return s + `"`
return s
}
func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
moov := MOOV()
mv := iso.NewMovie(1024)
mv.WriteFileType()
mv.StartAtom(iso.Moov)
mv.WriteMovieHeader()
for i, codec := range codecs {
switch codec.Name {
@@ -62,35 +71,11 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
return nil, err
}
width := codecData.Width()
height := codecData.Height()
trak := TRAK(i + 1)
trak.Header.TrackWidth = float64(width)
trak.Header.TrackHeight = float64(height)
trak.Media.Header.TimeScale = int32(codec.ClockRate)
trak.Media.Handler = &mp4io.HandlerRefer{
SubType: [4]byte{'v', 'i', 'd', 'e'},
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
}
trak.Media.Info.Video = &mp4io.VideoMediaInfo{
Flags: 0x000001,
}
trak.Media.Info.Sample.SampleDesc.AVC1Desc = &mp4io.AVC1Desc{
DataRefIdx: 1,
HorizontalResolution: 72,
VorizontalResolution: 72,
Width: int16(width),
Height: int16(height),
FrameCount: 1,
Depth: 24,
ColorTableId: -1,
Conf: &mp4io.AVC1Conf{
Data: codecData.AVCDecoderConfRecordBytes(),
},
}
moov.Tracks = append(moov.Tracks, trak)
mv.WriteVideoTrack(
uint32(i+1), codec.Name, codec.ClockRate,
uint16(codecData.Width()), uint16(codecData.Height()),
codecData.AVCDecoderConfRecordBytes(),
)
case streamer.CodecH265:
vps, sps, pps := h265.GetParameterSet(codec.FmtpLine)
@@ -106,35 +91,11 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
return nil, err
}
width := codecData.Width()
height := codecData.Height()
trak := TRAK(i + 1)
trak.Header.TrackWidth = float64(width)
trak.Header.TrackHeight = float64(height)
trak.Media.Header.TimeScale = int32(codec.ClockRate)
trak.Media.Handler = &mp4io.HandlerRefer{
SubType: [4]byte{'v', 'i', 'd', 'e'},
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
}
trak.Media.Info.Video = &mp4io.VideoMediaInfo{
Flags: 0x000001,
}
trak.Media.Info.Sample.SampleDesc.HV1Desc = &mp4io.HV1Desc{
DataRefIdx: 1,
HorizontalResolution: 72,
VorizontalResolution: 72,
Width: int16(width),
Height: int16(height),
FrameCount: 1,
Depth: 24,
ColorTableId: -1,
Conf: &mp4io.HV1Conf{
Data: codecData.AVCDecoderConfRecordBytes(),
},
}
moov.Tracks = append(moov.Tracks, trak)
mv.WriteVideoTrack(
uint32(i+1), codec.Name, codec.ClockRate,
uint16(codecData.Width()), uint16(codecData.Height()),
codecData.AVCDecoderConfRecordBytes(),
)
case streamer.CodecAAC:
s := streamer.Between(codec.FmtpLine, "config=", ";")
@@ -143,44 +104,29 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
return nil, err
}
trak := TRAK(i + 1)
trak.Header.AlternateGroup = 1
trak.Header.Duration = 0
trak.Header.Volume = 1
trak.Media.Header.TimeScale = int32(codec.ClockRate)
mv.WriteAudioTrack(
uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, b,
)
trak.Media.Handler = &mp4io.HandlerRefer{
SubType: [4]byte{'s', 'o', 'u', 'n'},
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
}
trak.Media.Info.Sound = &mp4io.SoundMediaInfo{}
trak.Media.Info.Sample.SampleDesc.MP4ADesc = &mp4io.MP4ADesc{
DataRefIdx: 1,
NumberOfChannels: int16(codec.Channels),
SampleSize: int16(av.FLTP.BytesPerSample() * 4),
SampleRate: float64(codec.ClockRate),
Unknowns: []mp4io.Atom{ESDS(b)},
}
moov.Tracks = append(moov.Tracks, trak)
case streamer.CodecOpus, streamer.CodecMP3, streamer.CodecPCMU, streamer.CodecPCMA:
mv.WriteAudioTrack(
uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, nil,
)
}
trex := &mp4io.TrackExtend{
TrackId: uint32(i + 1),
DefaultSampleDescIdx: 1,
DefaultSampleDuration: 0,
}
moov.MovieExtend.Tracks = append(moov.MovieExtend.Tracks, trex)
m.pts = append(m.pts, 0)
m.dts = append(m.dts, 0)
}
data := make([]byte, moov.Len())
moov.Marshal(data)
mv.StartAtom(iso.MoovMvex)
for i := range codecs {
mv.WriteTrackExtend(uint32(i + 1))
}
mv.EndAtom() // MVEX
return append(FTYP(), data...), nil
mv.EndAtom() // MOOV
return mv.Bytes(), nil
}
func (m *Muxer) Reset() {
@@ -192,65 +138,28 @@ func (m *Muxer) Reset() {
}
func (m *Muxer) Marshal(trackID byte, packet *rtp.Packet) []byte {
run := &mp4fio.TrackFragRun{
Flags: 0x000b05,
FirstSampleFlags: uint32(fmp4io.SampleNoDependencies),
DataOffset: 0,
Entries: []mp4io.TrackFragRunEntry{},
}
moof := &mp4fio.MovieFrag{
Header: &mp4fio.MovieFragHeader{
Seqnum: m.fragIndex + 1,
},
Tracks: []*mp4fio.TrackFrag{
{
Header: &mp4fio.TrackFragHeader{
Data: []byte{0x00, 0x02, 0x00, 0x20, 0x00, 0x00, 0x00, trackID + 1, 0x01, 0x01, 0x00, 0x00},
},
DecodeTime: &mp4fio.TrackFragDecodeTime{
Version: 1,
Flags: 0,
Time: m.dts[trackID],
},
Run: run,
},
},
}
entry := mp4io.TrackFragRunEntry{
Size: uint32(len(packet.Payload)),
}
newTime := packet.Timestamp
if m.pts[trackID] > 0 {
entry.Duration = newTime - m.pts[trackID]
m.dts[trackID] += uint64(entry.Duration)
} else {
// important, or Safari will fail with first frame
entry.Duration = 1
}
m.pts[trackID] = newTime
// important before moof.Len()
run.Entries = append(run.Entries, entry)
moofLen := moof.Len()
mdatLen := 8 + len(packet.Payload)
// important after moof.Len()
run.DataOffset = uint32(moofLen + 8)
buf := make([]byte, moofLen+mdatLen)
moof.Marshal(buf)
binary.BigEndian.PutUint32(buf[moofLen:], uint32(mdatLen))
copy(buf[moofLen+4:], "mdat")
copy(buf[moofLen+8:], packet.Payload)
// important before increment
time := m.dts[trackID]
m.fragIndex++
//m.total += moofLen + mdatLen
var duration uint32
newTime := packet.Timestamp
if m.pts[trackID] > 0 {
duration = newTime - m.pts[trackID]
m.dts[trackID] += uint64(duration)
} else {
// important, or Safari will fail with first frame
duration = 1
}
m.pts[trackID] = newTime
return buf
mv := iso.NewMovie(1024 + len(packet.Payload))
mv.WriteMovieFragment(
m.fragIndex, uint32(trackID+1), duration,
uint32(len(packet.Payload)), time,
)
mv.WriteData(packet.Payload)
return mv.Bytes()
}
+22 -2
View File
@@ -1,18 +1,25 @@
package mp4
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
"sync/atomic"
)
type Segment struct {
streamer.Element
Medias []*streamer.Media
Medias []*streamer.Media
UserAgent string
RemoteAddr string
MimeType string
OnlyKeyframe bool
send uint32
}
func (c *Segment) GetMedias() []*streamer.Media {
@@ -43,7 +50,7 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
return nil
}
c.MimeType = muxer.MimeType(codecs)
c.MimeType = `video/mp4; codecs="` + muxer.MimeCodecs(codecs) + `"`
switch track.Codec.Name {
case streamer.CodecH264:
@@ -56,6 +63,7 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
}
buf := muxer.Marshal(0, packet)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(append(init, buf...))
return nil
@@ -73,6 +81,7 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
buf = append(buf, b...)
}
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
buf = buf[:0]
@@ -106,6 +115,7 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
}
buf := muxer.Marshal(0, packet)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(append(init, buf...))
return nil
@@ -121,3 +131,13 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
panic("unsupported codec")
}
func (c *Segment) MarshalJSON() ([]byte, error) {
info := &streamer.Info{
Type: "WS/MP4 client",
RemoteAddr: c.RemoteAddr,
UserAgent: c.UserAgent,
Send: atomic.LoadUint32(&c.send),
}
return json.Marshal(info)
}
+25 -17
View File
@@ -1,7 +1,8 @@
package mp4f
package mp4
import (
"encoding/json"
"encoding/hex"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/av"
@@ -15,6 +16,7 @@ import (
type Consumer struct {
streamer.Element
Medias []*streamer.Media
UserAgent string
RemoteAddr string
@@ -27,6 +29,10 @@ type Consumer struct {
}
func (c *Consumer) GetMedias() []*streamer.Media {
if c.Medias != nil {
return c.Medias
}
return []*streamer.Media{
{
Kind: streamer.KindVideo,
@@ -89,7 +95,7 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
return nil
}
if !codec.IsRAW() {
if codec.IsRTP() {
wrapper := h264.RTPDepay(track)
push = wrapper(push)
}
@@ -97,7 +103,17 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
return track.Bind(push)
case streamer.CodecAAC:
stream, _ := aacparser.NewCodecDataFromMPEG4AudioConfigBytes([]byte{20, 8})
s := streamer.Between(codec.FmtpLine, "config=", ";")
b, err := hex.DecodeString(s)
if err != nil {
return nil
}
stream, err := aacparser.NewCodecDataFromMPEG4AudioConfigBytes(b)
if err != nil {
return nil
}
c.mimeType += ",mp4a.40.2"
c.streams = append(c.streams, stream)
@@ -127,6 +143,11 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
return nil
}
if codec.IsRTP() {
wrapper := aac.RTPDepay(track)
push = wrapper(push)
}
return track.Bind(push)
}
@@ -149,16 +170,3 @@ func (c *Consumer) Init() ([]byte, error) {
func (c *Consumer) Start() {
c.start = true
}
//
func (c *Consumer) MarshalJSON() ([]byte, error) {
v := map[string]interface{}{
"type": "MSE server consumer",
"send": c.send,
"remote_addr": c.RemoteAddr,
"user_agent": c.UserAgent,
}
return json.Marshal(v)
}
+174
View File
@@ -0,0 +1,174 @@
package mp4
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
"sync/atomic"
)
type Consumer struct {
streamer.Element
Medias []*streamer.Media
UserAgent string
RemoteAddr string
muxer *Muxer
codecs []*streamer.Codec
wait byte
send uint32
}
const (
waitNone byte = iota
waitKeyframe
waitInit
)
func (c *Consumer) GetMedias() []*streamer.Media {
if c.Medias != nil {
return c.Medias
}
// default medias
return []*streamer.Media{
{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecH264},
{Name: streamer.CodecH265},
},
},
{
Kind: streamer.KindAudio,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecAAC},
},
},
}
}
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
trackID := byte(len(c.codecs))
c.codecs = append(c.codecs, track.Codec)
codec := track.Codec
switch codec.Name {
case streamer.CodecH264:
c.wait = waitInit
push := func(packet *rtp.Packet) error {
if packet.Version != h264.RTPPacketVersionAVC {
return nil
}
if c.wait != waitNone {
if c.wait == waitInit || !h264.IsKeyframe(packet.Payload) {
return nil
}
c.wait = waitNone
}
buf := c.muxer.Marshal(trackID, packet)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
return nil
}
var wrapper streamer.WrapperFunc
if codec.IsRTP() {
wrapper = h264.RTPDepay(track)
} else {
wrapper = h264.RepairAVC(track)
}
push = wrapper(push)
return track.Bind(push)
case streamer.CodecH265:
c.wait = waitInit
push := func(packet *rtp.Packet) error {
if packet.Version != h264.RTPPacketVersionAVC {
return nil
}
if c.wait != waitNone {
if c.wait == waitInit || !h265.IsKeyframe(packet.Payload) {
return nil
}
c.wait = waitNone
}
buf := c.muxer.Marshal(trackID, packet)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
return nil
}
if codec.IsRTP() {
wrapper := h265.RTPDepay(track)
push = wrapper(push)
}
return track.Bind(push)
case streamer.CodecAAC:
push := func(packet *rtp.Packet) error {
if c.wait != waitNone {
return nil
}
buf := c.muxer.Marshal(trackID, packet)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
return nil
}
if codec.IsRTP() {
wrapper := aac.RTPDepay(track)
push = wrapper(push)
}
return track.Bind(push)
}
panic("unsupported codec")
}
func (c *Consumer) MimeType() string {
return c.muxer.MimeType(c.codecs)
}
func (c *Consumer) Init() ([]byte, error) {
c.muxer = &Muxer{}
return c.muxer.GetInit(c.codecs)
}
func (c *Consumer) Start() {
if c.wait == waitInit {
c.wait = waitKeyframe
}
}
//
func (c *Consumer) MarshalJSON() ([]byte, error) {
info := &streamer.Info{
Type: "MP4 client",
RemoteAddr: c.RemoteAddr,
UserAgent: c.UserAgent,
Send: atomic.LoadUint32(&c.send),
}
return json.Marshal(info)
}
+256
View File
@@ -0,0 +1,256 @@
package mp4
import (
"encoding/binary"
"encoding/hex"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/codec/h265parser"
"github.com/deepch/vdk/format/fmp4/fmp4io"
"github.com/deepch/vdk/format/mp4/mp4io"
"github.com/deepch/vdk/format/mp4f/mp4fio"
"github.com/pion/rtp"
)
type Muxer struct {
fragIndex uint32
dts []uint64
pts []uint32
}
func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
s := `video/mp4; codecs="`
for i, codec := range codecs {
if i > 0 {
s += ","
}
switch codec.Name {
case streamer.CodecH264:
s += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
case streamer.CodecH265:
// H.265 profile=main level=5.1
// hvc1 - supported in Safari, hev1 - doesn't, both supported in Chrome
s += "hvc1.1.6.L153.B0"
case streamer.CodecAAC:
s += "mp4a.40.2"
}
}
return s + `"`
}
func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
moov := MOOV()
for i, codec := range codecs {
switch codec.Name {
case streamer.CodecH264:
sps, pps := h264.GetParameterSet(codec.FmtpLine)
if sps == nil {
// some dummy SPS and PPS not a problem
sps = []byte{0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2}
pps = []byte{0x68, 0xce, 0x38, 0x80}
}
codecData, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
if err != nil {
return nil, err
}
width := codecData.Width()
height := codecData.Height()
trak := TRAK(i + 1)
trak.Header.TrackWidth = float64(width)
trak.Header.TrackHeight = float64(height)
trak.Media.Header.TimeScale = int32(codec.ClockRate)
trak.Media.Handler = &mp4io.HandlerRefer{
SubType: [4]byte{'v', 'i', 'd', 'e'},
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
}
trak.Media.Info.Video = &mp4io.VideoMediaInfo{
Flags: 0x000001,
}
trak.Media.Info.Sample.SampleDesc.AVC1Desc = &mp4io.AVC1Desc{
DataRefIdx: 1,
HorizontalResolution: 72,
VorizontalResolution: 72,
Width: int16(width),
Height: int16(height),
FrameCount: 1,
Depth: 24,
ColorTableId: -1,
Conf: &mp4io.AVC1Conf{
Data: codecData.AVCDecoderConfRecordBytes(),
},
}
moov.Tracks = append(moov.Tracks, trak)
case streamer.CodecH265:
vps, sps, pps := h265.GetParameterSet(codec.FmtpLine)
if sps == nil {
// some dummy SPS and PPS not a problem
vps = []byte{0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x99, 0xac, 0x09}
sps = []byte{0x42, 0x01, 0x01, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x99, 0xa0, 0x01, 0x40, 0x20, 0x05, 0xa1, 0xfe, 0x5a, 0xee, 0x46, 0xc1, 0xae, 0x55, 0x04}
pps = []byte{0x44, 0x01, 0xc0, 0x73, 0xc0, 0x4c, 0x90}
}
codecData, err := h265parser.NewCodecDataFromVPSAndSPSAndPPS(vps, sps, pps)
if err != nil {
return nil, err
}
width := codecData.Width()
height := codecData.Height()
trak := TRAK(i + 1)
trak.Header.TrackWidth = float64(width)
trak.Header.TrackHeight = float64(height)
trak.Media.Header.TimeScale = int32(codec.ClockRate)
trak.Media.Handler = &mp4io.HandlerRefer{
SubType: [4]byte{'v', 'i', 'd', 'e'},
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
}
trak.Media.Info.Video = &mp4io.VideoMediaInfo{
Flags: 0x000001,
}
trak.Media.Info.Sample.SampleDesc.HV1Desc = &mp4io.HV1Desc{
DataRefIdx: 1,
HorizontalResolution: 72,
VorizontalResolution: 72,
Width: int16(width),
Height: int16(height),
FrameCount: 1,
Depth: 24,
ColorTableId: -1,
Conf: &mp4io.HV1Conf{
Data: codecData.AVCDecoderConfRecordBytes(),
},
}
moov.Tracks = append(moov.Tracks, trak)
case streamer.CodecAAC:
s := streamer.Between(codec.FmtpLine, "config=", ";")
b, err := hex.DecodeString(s)
if err != nil {
return nil, err
}
trak := TRAK(i + 1)
trak.Header.AlternateGroup = 1
trak.Header.Duration = 0
trak.Header.Volume = 1
trak.Media.Header.TimeScale = int32(codec.ClockRate)
trak.Media.Handler = &mp4io.HandlerRefer{
SubType: [4]byte{'s', 'o', 'u', 'n'},
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
}
trak.Media.Info.Sound = &mp4io.SoundMediaInfo{}
trak.Media.Info.Sample.SampleDesc.MP4ADesc = &mp4io.MP4ADesc{
DataRefIdx: 1,
NumberOfChannels: int16(codec.Channels),
SampleSize: int16(av.FLTP.BytesPerSample() * 4),
SampleRate: float64(codec.ClockRate),
Unknowns: []mp4io.Atom{ESDS(b)},
}
moov.Tracks = append(moov.Tracks, trak)
}
trex := &mp4io.TrackExtend{
TrackId: uint32(i + 1),
DefaultSampleDescIdx: 1,
DefaultSampleDuration: 0,
}
moov.MovieExtend.Tracks = append(moov.MovieExtend.Tracks, trex)
m.pts = append(m.pts, 0)
m.dts = append(m.dts, 0)
}
data := make([]byte, moov.Len())
moov.Marshal(data)
return append(FTYP(), data...), nil
}
func (m *Muxer) Reset() {
m.fragIndex = 0
for i := range m.dts {
m.dts[i] = 0
m.pts[i] = 0
}
}
func (m *Muxer) Marshal(trackID byte, packet *rtp.Packet) []byte {
run := &mp4fio.TrackFragRun{
Flags: 0x000b05,
FirstSampleFlags: uint32(fmp4io.SampleNoDependencies),
DataOffset: 0,
Entries: []mp4io.TrackFragRunEntry{},
}
moof := &mp4fio.MovieFrag{
Header: &mp4fio.MovieFragHeader{
Seqnum: m.fragIndex + 1,
},
Tracks: []*mp4fio.TrackFrag{
{
Header: &mp4fio.TrackFragHeader{
Data: []byte{0x00, 0x02, 0x00, 0x20, 0x00, 0x00, 0x00, trackID + 1, 0x01, 0x01, 0x00, 0x00},
},
DecodeTime: &mp4fio.TrackFragDecodeTime{
Version: 1,
Flags: 0,
Time: m.dts[trackID],
},
Run: run,
},
},
}
entry := mp4io.TrackFragRunEntry{
Size: uint32(len(packet.Payload)),
}
newTime := packet.Timestamp
if m.pts[trackID] > 0 {
entry.Duration = newTime - m.pts[trackID]
m.dts[trackID] += uint64(entry.Duration)
} else {
// important, or Safari will fail with first frame
entry.Duration = 1
}
m.pts[trackID] = newTime
// important before moof.Len()
run.Entries = append(run.Entries, entry)
moofLen := moof.Len()
mdatLen := 8 + len(packet.Payload)
// important after moof.Len()
run.DataOffset = uint32(moofLen + 8)
buf := make([]byte, moofLen+mdatLen)
moof.Marshal(buf)
binary.BigEndian.PutUint32(buf[moofLen:], uint32(mdatLen))
copy(buf[moofLen+4:], "mdat")
copy(buf[moofLen+8:], packet.Payload)
m.fragIndex++
//m.total += moofLen + mdatLen
return buf
}
+143
View File
@@ -0,0 +1,143 @@
package mp4
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
"sync/atomic"
)
type Segment struct {
streamer.Element
Medias []*streamer.Media
UserAgent string
RemoteAddr string
MimeType string
OnlyKeyframe bool
send uint32
}
func (c *Segment) GetMedias() []*streamer.Media {
if c.Medias != nil {
return c.Medias
}
// default medias
return []*streamer.Media{
{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecH264},
{Name: streamer.CodecH265},
},
},
}
}
func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
muxer := &Muxer{}
codecs := []*streamer.Codec{track.Codec}
init, err := muxer.GetInit(codecs)
if err != nil {
return nil
}
c.MimeType = muxer.MimeType(codecs)
switch track.Codec.Name {
case streamer.CodecH264:
var push streamer.WriterFunc
if c.OnlyKeyframe {
push = func(packet *rtp.Packet) error {
if !h264.IsKeyframe(packet.Payload) {
return nil
}
buf := muxer.Marshal(0, packet)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(append(init, buf...))
return nil
}
} else {
var buf []byte
push = func(packet *rtp.Packet) error {
if h264.IsKeyframe(packet.Payload) {
// fist frame - send only IFrame
// other frames - send IFrame and all PFrames
if buf == nil {
buf = append(buf, init...)
b := muxer.Marshal(0, packet)
buf = append(buf, b...)
}
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
buf = buf[:0]
buf = append(buf, init...)
muxer.Reset()
}
if buf != nil {
b := muxer.Marshal(0, packet)
buf = append(buf, b...)
}
return nil
}
}
var wrapper streamer.WrapperFunc
if track.Codec.IsRTP() {
wrapper = h264.RTPDepay(track)
} else {
wrapper = h264.RepairAVC(track)
}
push = wrapper(push)
return track.Bind(push)
case streamer.CodecH265:
push := func(packet *rtp.Packet) error {
if !h265.IsKeyframe(packet.Payload) {
return nil
}
buf := muxer.Marshal(0, packet)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(append(init, buf...))
return nil
}
if track.Codec.IsRTP() {
wrapper := h265.RTPDepay(track)
push = wrapper(push)
}
return track.Bind(push)
}
panic("unsupported codec")
}
func (c *Segment) MarshalJSON() ([]byte, error) {
info := &streamer.Info{
Type: "WS/MP4 client",
RemoteAddr: c.RemoteAddr,
UserAgent: c.UserAgent,
Send: atomic.LoadUint32(&c.send),
}
return json.Marshal(info)
}
+3 -2
View File
@@ -12,6 +12,7 @@ import (
"github.com/deepch/vdk/format/rtmp"
"github.com/pion/rtp"
"net/http"
"sync/atomic"
"time"
)
@@ -33,7 +34,7 @@ type Client struct {
conn Conn
closed bool
receive int
recv uint32
}
func NewClient(uri string) *Client {
@@ -138,7 +139,7 @@ func (c *Client) Handle() (err error) {
return
}
c.receive += len(pkt.Data)
atomic.AddUint32(&c.recv, uint32(len(pkt.Data)))
track := c.tracks[int(pkt.Idx)]
+8 -15
View File
@@ -4,7 +4,7 @@ import (
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strconv"
"sync/atomic"
)
func (c *Client) GetMedias() []*streamer.Media {
@@ -29,19 +29,12 @@ func (c *Client) Stop() error {
}
func (c *Client) MarshalJSON() ([]byte, error) {
v := map[string]interface{}{
streamer.JSONReceive: c.receive,
streamer.JSONType: "RTMP client producer",
//streamer.JSONRemoteAddr: c.conn.NetConn().RemoteAddr().String(),
"url": c.URI,
info := &streamer.Info{
Type: "RTMP source",
URL: c.URI,
Medias: c.medias,
Tracks: c.tracks,
Recv: atomic.LoadUint32(&c.recv),
}
for i, media := range c.medias {
k := "media:" + strconv.Itoa(i)
v[k] = media.String()
}
for i, track := range c.tracks {
k := "track:" + strconv.Itoa(i)
v[k] = track.String()
}
return json.Marshal(v)
return json.Marshal(info)
}
+140 -73
View File
@@ -2,7 +2,6 @@ package rtsp
import (
"bufio"
"bytes"
"crypto/tls"
"encoding/binary"
"errors"
@@ -20,6 +19,7 @@ import (
"net/url"
"strconv"
"strings"
"sync"
"time"
)
@@ -47,11 +47,28 @@ const (
type State byte
func (s State) String() string {
switch s {
case StateNone:
return "NONE"
case StateConn:
return "CONN"
case StateSetup:
return "SETUP"
case StatePlay:
return "PLAY"
case StateHandle:
return "HANDLE"
}
return strconv.Itoa(int(s))
}
const (
StateNone State = iota
StateConn
StateSetup
StatePlay
StateHandle
)
type Conn struct {
@@ -60,6 +77,7 @@ type Conn struct {
// public
Backchannel bool
SessionName string
Medias []*streamer.Media
Session string
@@ -72,6 +90,7 @@ type Conn struct {
conn net.Conn
mode Mode
state State
stateMu sync.Mutex
reader *bufio.Reader
sequence int
uri string
@@ -261,7 +280,7 @@ func (c *Conn) Options() error {
}
if val := res.Header.Get("Content-Base"); val != "" {
c.URL, err = url.Parse(val)
c.URL, err = urlParse(val)
if err != nil {
return err
}
@@ -285,13 +304,19 @@ func (c *Conn) Describe() error {
req.Header.Set("Require", "www.onvif.org/ver20/backchannel")
}
if c.UserAgent != "" {
// this camera will answer with 401 on DESCRIBE without User-Agent
// https://github.com/AlexxIT/go2rtc/issues/235
req.Header.Set("User-Agent", c.UserAgent)
}
res, err := c.Do(req)
if err != nil {
return err
}
if val := res.Header.Get("Content-Base"); val != "" {
c.URL, err = url.Parse(val)
c.URL, err = urlParse(val)
if err != nil {
return err
}
@@ -328,7 +353,7 @@ func (c *Conn) Describe() error {
func (c *Conn) Setup() error {
for _, media := range c.Medias {
_, err := c.SetupMedia(media, media.Codecs[0])
_, err := c.SetupMedia(media, media.Codecs[0], true)
if err != nil {
return err
}
@@ -337,9 +362,17 @@ func (c *Conn) Setup() error {
return nil
}
func (c *Conn) SetupMedia(
media *streamer.Media, codec *streamer.Codec,
) (*streamer.Track, error) {
func (c *Conn) SetupMedia(media *streamer.Media, codec *streamer.Codec, first bool) (*streamer.Track, error) {
// TODO: rewrite recoonection and first flag
if first {
c.stateMu.Lock()
defer c.stateMu.Unlock()
}
if c.state != StateConn && c.state != StateSetup {
return nil, fmt.Errorf("RTSP SETUP from wrong state: %s", c.state)
}
ch := c.GetChannel(media)
if ch < 0 {
return nil, fmt.Errorf("wrong media: %v", media)
@@ -353,7 +386,7 @@ func (c *Conn) SetupMedia(
}
rawURL += media.Control
}
trackURL, err := url.Parse(rawURL)
trackURL, err := urlParse(rawURL)
if err != nil {
return nil, err
}
@@ -386,7 +419,7 @@ func (c *Conn) SetupMedia(
for _, newMedia := range c.Medias {
if newMedia.Control == media.Control {
return c.SetupMedia(newMedia, newMedia.Codecs[0])
return c.SetupMedia(newMedia, newMedia.Codecs[0], false)
}
}
}
@@ -461,12 +494,19 @@ func (c *Conn) SetupMedia(
}
func (c *Conn) Play() (err error) {
c.stateMu.Lock()
defer c.stateMu.Unlock()
if c.state != StateSetup {
return fmt.Errorf("RTSP PLAY from wrong state: %s", c.state)
}
req := &tcp.Request{Method: MethodPlay, URL: c.URL}
return c.Request(req)
if err = c.Request(req); err == nil {
c.state = StatePlay
}
return
}
func (c *Conn) Teardown() (err error) {
@@ -476,12 +516,14 @@ func (c *Conn) Teardown() (err error) {
}
func (c *Conn) Close() error {
c.stateMu.Lock()
defer c.stateMu.Unlock()
if c.state == StateNone {
return nil
}
if err := c.Teardown(); err != nil {
return err
}
_ = c.Teardown()
c.state = StateNone
return c.conn.Close()
}
@@ -583,7 +625,7 @@ func (c *Conn) Accept() error {
medias = append(medias, media)
}
res.Body, err = streamer.MarshalSDP(medias)
res.Body, err = streamer.MarshalSDP(c.SessionName, medias)
if err != nil {
return err
}
@@ -614,7 +656,16 @@ func (c *Conn) Accept() error {
case MethodRecord, MethodPlay:
res := &tcp.Response{Request: req}
return c.Response(res)
if err = c.Response(res); err == nil {
c.state = StatePlay
}
return err
case MethodTeardown:
res := &tcp.Response{Request: req}
_ = c.Response(res)
c.state = StateNone
return c.conn.Close()
default:
return fmt.Errorf("unsupported method: %s", req.Method)
@@ -623,13 +674,31 @@ func (c *Conn) Accept() error {
}
func (c *Conn) Handle() (err error) {
if c.state != StateSetup {
return fmt.Errorf("RTSP Handle from wrong state: %d", c.state)
c.stateMu.Lock()
switch c.state {
case StateNone: // Close after PLAY and before Handle is OK (because SETUP after PLAY)
case StatePlay:
c.state = StateHandle
default:
err = fmt.Errorf("RTSP HANDLE from wrong state: %s", c.state)
c.state = StateNone
_ = c.conn.Close()
}
c.state = StatePlay
ok := c.state == StateHandle
c.stateMu.Unlock()
if !ok {
return
}
defer func() {
c.stateMu.Lock()
defer c.stateMu.Unlock()
if c.state == StateNone {
err = nil
return
@@ -680,6 +749,9 @@ func (c *Conn) Handle() (err error) {
return
}
var channelID byte
var size uint16
if buf4[0] != '$' {
switch string(buf4) {
case "RTSP":
@@ -688,26 +760,62 @@ func (c *Conn) Handle() (err error) {
return
}
c.Fire(res)
continue
case "OPTI", "TEAR", "DESC", "SETU", "PLAY", "PAUS", "RECO", "ANNO", "GET_", "SET_":
var req *tcp.Request
if req, err = tcp.ReadRequest(c.reader); err != nil {
return
}
c.Fire(req)
continue
default:
return fmt.Errorf("RTSP wrong input")
for i := 0; ; i++ {
// search next start symbol
if _, err = c.reader.ReadBytes('$'); err != nil {
return err
}
if channelID, err = c.reader.ReadByte(); err != nil {
return err
}
// check if channel ID exists
if c.channels[channelID] == nil {
continue
}
buf4 = make([]byte, 2)
if _, err = io.ReadFull(c.reader, buf4); err != nil {
return err
}
// check if size good for RTP
size = binary.BigEndian.Uint16(buf4)
if size <= 1500 {
break
}
// 10 tries to find good packet
if i >= 10 {
return fmt.Errorf("RTSP wrong input")
}
}
c.Fire("RTSP wrong input")
}
continue
}
} else {
// hope that the odd channels are always RTCP
channelID = buf4[1]
// hope that the odd channels are always RTCP
channelID := buf4[1]
// get data size
size = binary.BigEndian.Uint16(buf4[2:])
// get data size
size := int(binary.BigEndian.Uint16(buf4[2:]))
if _, err = c.reader.Discard(4); err != nil {
return
// skip 4 bytes from c.reader.Peek
if _, err = c.reader.Discard(4); err != nil {
return
}
}
// init memory for data
@@ -716,7 +824,7 @@ func (c *Conn) Handle() (err error) {
return
}
c.receive += size
c.receive += int(size)
if channelID&1 == 0 {
packet := &rtp.Packet{}
@@ -727,21 +835,19 @@ func (c *Conn) Handle() (err error) {
track := c.channels[channelID]
if track != nil {
_ = track.WriteRTP(packet)
//return fmt.Errorf("wrong channelID: %d", channelID)
} else {
continue // TODO: maybe fix this
//panic("wrong channelID")
//c.Fire("wrong channelID: " + strconv.Itoa(int(channelID)))
}
} else {
msg := &RTCP{Channel: channelID}
if err = msg.Header.Unmarshal(buf); err != nil {
return
continue
}
msg.Packets, err = rtcp.Unmarshal(buf)
if err != nil {
return
continue
}
c.Fire(msg)
@@ -819,42 +925,3 @@ func (c *Conn) bindTrack(
return track.Bind(push)
}
type RTCP struct {
Channel byte
Header rtcp.Header
Packets []rtcp.Packet
}
const sdpHeader = `v=0
o=- 0 0 IN IP4 0.0.0.0
s=-
t=0 0`
func UnmarshalSDP(rawSDP []byte) ([]*streamer.Media, error) {
medias, err := streamer.UnmarshalSDP(rawSDP)
if err != nil {
// fix SDP header for some cameras
i := bytes.Index(rawSDP, []byte("\nm="))
if i > 0 {
rawSDP = append([]byte(sdpHeader), rawSDP[i:]...)
medias, err = streamer.UnmarshalSDP(rawSDP)
}
if err != nil {
return nil, err
}
}
// fix bug in ONVIF spec
// https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec-v241.pdf
for _, media := range medias {
switch media.Direction {
case streamer.DirectionRecvonly, "":
media.Direction = streamer.DirectionSendonly
case streamer.DirectionSendonly:
media.Direction = streamer.DirectionRecvonly
}
}
return medias, nil
}
+103
View File
@@ -0,0 +1,103 @@
package rtsp
import (
"bytes"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtcp"
"github.com/pion/sdp/v3"
"net/url"
"regexp"
"strconv"
"strings"
)
type RTCP struct {
Channel byte
Header rtcp.Header
Packets []rtcp.Packet
}
const sdpHeader = `v=0
o=- 0 0 IN IP4 0.0.0.0
s=-
t=0 0`
func UnmarshalSDP(rawSDP []byte) ([]*streamer.Media, error) {
// fix bug from Reolink Doorbell
if i := bytes.Index(rawSDP, []byte("a=sendonlym=")); i > 0 {
rawSDP = append(rawSDP[:i+11], rawSDP[i+10:]...)
rawSDP[i+10] = '\n'
}
sd := &sdp.SessionDescription{}
if err := sd.Unmarshal(rawSDP); err != nil {
// fix multiple `s=` https://github.com/AlexxIT/WebRTC/issues/417
re, _ := regexp.Compile("\ns=[^\n]+")
rawSDP = re.ReplaceAll(rawSDP, nil)
// fix SDP header for some cameras
if i := bytes.Index(rawSDP, []byte("\nm=")); i > 0 {
rawSDP = append([]byte(sdpHeader), rawSDP[i:]...)
sd = &sdp.SessionDescription{}
err = sd.Unmarshal(rawSDP)
}
if err != nil {
return nil, err
}
}
medias := streamer.UnmarshalMedias(sd.MediaDescriptions)
for _, media := range medias {
// Check buggy SDP with fmtp for H264 on another track
// https://github.com/AlexxIT/WebRTC/issues/419
for _, codec := range media.Codecs {
if codec.Name == streamer.CodecH264 && codec.FmtpLine == "" {
codec.FmtpLine = findFmtpLine(codec.PayloadType, sd.MediaDescriptions)
}
}
// fix bug in ONVIF spec
// https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec-v241.pdf
switch media.Direction {
case streamer.DirectionRecvonly, "":
media.Direction = streamer.DirectionSendonly
case streamer.DirectionSendonly:
media.Direction = streamer.DirectionRecvonly
}
}
return medias, nil
}
func findFmtpLine(payloadType uint8, descriptions []*sdp.MediaDescription) string {
s := strconv.Itoa(int(payloadType))
for _, md := range descriptions {
codec := streamer.UnmarshalCodec(md, s)
if codec.FmtpLine != "" {
return codec.FmtpLine
}
}
return ""
}
// urlParse fix bugs:
// 1. Content-Base: rtsp://::ffff:192.168.1.123/onvif/profile.1/
// 2. Content-Base: rtsp://rtsp://turret2-cam.lan:554/stream1/
func urlParse(rawURL string) (*url.URL, error) {
if strings.HasPrefix(rawURL, "rtsp://rtsp://") {
rawURL = rawURL[7:]
}
u, err := url.Parse(rawURL)
if err != nil && strings.HasSuffix(err.Error(), "after host") {
if i1 := strings.Index(rawURL, "://"); i1 > 0 {
if i2 := strings.IndexByte(rawURL[i1+3:], '/'); i2 > 0 {
return urlParse(rawURL[:i1+3+i2] + ":" + rawURL[i1+3+i2:])
}
}
}
return u, err
}
+108
View File
@@ -0,0 +1,108 @@
package rtsp
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestURLParse(t *testing.T) {
// https://github.com/AlexxIT/WebRTC/issues/395
base := "rtsp://::ffff:192.168.1.123/onvif/profile.1/"
u, err := urlParse(base)
assert.Empty(t, err)
assert.Equal(t, "::ffff:192.168.1.123:", u.Host)
// https://github.com/AlexxIT/go2rtc/issues/208
base = "rtsp://rtsp://turret2-cam.lan:554/stream1/"
u, err = urlParse(base)
assert.Empty(t, err)
assert.Equal(t, "turret2-cam.lan:554", u.Host)
}
func TestBugSDP1(t *testing.T) {
// https://github.com/AlexxIT/WebRTC/issues/417
s := `v=0
o=- 91674849066 1 IN IP4 192.168.1.123
s=RtspServer
i=live
t=0 0
a=control:*
a=range:npt=0-
m=video 0 RTP/AVP 96
c=IN IP4 0.0.0.0
s=RtspServer
i=live
a=control:track0
a=range:npt=0-
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1;profile-level-id=42001E;sprop-parameter-sets=Z0IAHvQCgC3I,aM48gA==
a=control:track0
m=audio 0 RTP/AVP 97
c=IN IP4 0.0.0.0
s=RtspServer
i=live
a=control:track1
a=range:npt=0-
a=rtpmap:97 MPEG4-GENERIC/8000/1
a=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1588
a=control:track1
`
medias, err := UnmarshalSDP([]byte(s))
assert.Nil(t, err)
assert.NotNil(t, medias)
}
func TestBugSDP2(t *testing.T) {
// https://github.com/AlexxIT/WebRTC/issues/419
s := `v=0
o=- 1675628282 1675628283 IN IP4 192.168.1.123
s=streamed by the RTSP server
t=0 0
m=video 0 RTP/AVP 96
a=rtpmap:96 H264/90000
a=control:track0
m=audio 0 RTP/AVP 8
a=rtpmap:0 pcma/8000/1
a=control:track1
a=framerate:25
a=range:npt=now-
a=fmtp:96 packetization-mode=1;profile-level-id=64001F;sprop-parameter-sets=Z0IAH5WoFAFuQA==,aM48gA==
`
medias, err := UnmarshalSDP([]byte(s))
assert.Nil(t, err)
assert.NotNil(t, medias)
assert.NotEqual(t, "", medias[0].Codecs[0].FmtpLine)
}
func TestBugSDP3(t *testing.T) {
s := `v=0
o=- 1675775048103026 1 IN IP4 192.168.1.123
s=Session streamed by "preview"
t=0 0
a=tool:LIVE555 Streaming Media v2020.08.12
a=type:broadcast
a=control:*
a=range:npt=0-
a=x-qt-text-nam:Session streamed by "preview"
m=video 0 RTP/AVP 96
c=IN IP4 0.0.0.0
b=AS:8192
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1;profile-level-id=640033;sprop-parameter-sets=Z2QAM6wVFKAoAPGQ,aO48sA==
a=recvonly
a=control:track1
m=audio 0 RTP/AVP 8
a=control:track2
a=rtpmap:8 PCMA/8000
a=sendonlym=audio 0 RTP/AVP 98
c=IN IP4 0.0.0.0
b=AS:8192
a=rtpmap:98 MPEG4-GENERIC/16000
a=fmtp:98 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408;
a=recvonly
a=control:track3
`
medias, err := UnmarshalSDP([]byte(s))
assert.Nil(t, err)
assert.Len(t, medias, 3)
}
+54 -35
View File
@@ -4,13 +4,31 @@ import (
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strconv"
)
// Element Producer
func (c *Conn) GetMedias() []*streamer.Media {
return c.Medias
if c.Medias != nil {
return c.Medias
}
return []*streamer.Media{
{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecAll},
},
},
{
Kind: streamer.KindAudio,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecAll},
},
},
}
}
func (c *Conn) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
@@ -21,12 +39,13 @@ func (c *Conn) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.
}
// can't setup new tracks from play state - forcing a reconnection feature
if c.state == StatePlay {
switch c.state {
case StatePlay, StateHandle:
go c.Close()
return streamer.NewTrack(codec, media.Direction)
}
track, err := c.SetupMedia(media, codec)
track, err := c.SetupMedia(media, codec, true)
if err != nil {
return nil
}
@@ -63,11 +82,20 @@ func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.
codec := track.Codec.Clone()
codec.PayloadType = uint8(96 + i)
for i, m := range c.Medias {
if m == media {
media.Codecs = []*streamer.Codec{codec}
c.Medias[i] = media
break
if media.MatchAll() {
// fill consumer medias list
c.Medias = append(c.Medias, &streamer.Media{
Kind: media.Kind, Direction: media.Direction,
Codecs: []*streamer.Codec{codec},
})
} else {
// find consumer media and replace codec with right one
for i, m := range c.Medias {
if m == media {
media.Codecs = []*streamer.Codec{codec}
c.Medias[i] = media
break
}
}
}
@@ -87,40 +115,30 @@ func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.
//
func (c *Conn) MarshalJSON() ([]byte, error) {
v := map[string]interface{}{
streamer.JSONReceive: c.receive,
streamer.JSONSend: c.send,
info := &streamer.Info{
UserAgent: c.UserAgent,
Medias: c.Medias,
Tracks: c.tracks,
Recv: uint32(c.receive),
Send: uint32(c.send),
}
switch c.mode {
case ModeUnknown:
v[streamer.JSONType] = "RTSP unknown"
case ModeClientProducer:
v[streamer.JSONType] = "RTSP client producer"
case ModeServerProducer:
v[streamer.JSONType] = "RTSP server producer"
info.Type = "RTSP unknown"
case ModeClientProducer, ModeServerProducer:
info.Type = "RTSP source"
case ModeServerConsumer:
v[streamer.JSONType] = "RTSP server consumer"
info.Type = "RTSP client"
}
//if c.URI != "" {
// v["uri"] = c.URI
//}
if c.URL != nil {
v["url"] = c.URL.String()
info.URL = c.URL.String()
}
if c.conn != nil {
v[streamer.JSONRemoteAddr] = c.conn.RemoteAddr().String()
}
if c.UserAgent != "" {
v[streamer.JSONUserAgent] = c.UserAgent
}
for i, media := range c.Medias {
k := "media:" + strconv.Itoa(i)
v[k] = media.String()
}
for i, track := range c.tracks {
k := "track:" + strconv.Itoa(int(i>>1))
v[k] = track.String()
info.RemoteAddr = c.conn.RemoteAddr().String()
}
//for i, track := range c.tracks {
// k := "track:" + strconv.Itoa(i+1)
// if track.MimeType() == streamer.MimeTypeH264 {
@@ -129,5 +147,6 @@ func (c *Conn) MarshalJSON() ([]byte, error) {
// v[k] = track.MimeType()
// }
//}
return json.Marshal(v)
return json.Marshal(info)
}
+32
View File
@@ -0,0 +1,32 @@
package shell
import (
"os"
"regexp"
"strings"
)
func ReplaceEnvVars(text string) string {
re := regexp.MustCompile(`\${([^}{]+)}`)
return re.ReplaceAllStringFunc(text, func(match string) string {
key := match[2 : len(match)-1]
var def string
var dok bool
if i := strings.IndexByte(key, ':'); i > 0 {
key, def = key[:i], key[i+1:]
dok = true
}
if value, vok := os.LookupEnv(key); vok {
return value
}
if dok {
return def
}
return match
})
}
+3
View File
@@ -3,6 +3,7 @@ package srtp
import (
"encoding/binary"
"net"
"sync/atomic"
)
// Server using same UDP port for SRTP and for SRTCP as the iPhone does
@@ -55,6 +56,8 @@ func (s *Server) Serve(conn net.PacketConn) error {
}
}
atomic.AddUint32(&session.Recv, uint32(n))
if err = session.HandleRTP(buf[:n]); err != nil {
return err
}
+1
View File
@@ -17,6 +17,7 @@ type Session struct {
Write func(b []byte) (int, error)
Track *streamer.Track
Recv uint32
lastSequence uint32
lastTimestamp uint32
+10 -7
View File
@@ -4,13 +4,16 @@ import (
"strings"
)
const (
JSONType = "type"
JSONRemoteAddr = "remote_addr"
JSONUserAgent = "user_agent"
JSONReceive = "receive"
JSONSend = "send"
)
type Info struct {
Type string `json:"type,omitempty"`
URL string `json:"url,omitempty"`
RemoteAddr string `json:"remote_addr,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
Medias []*Media `json:"medias,omitempty"`
Tracks []*Track `json:"tracks,omitempty"`
Recv uint32 `json:"recv,omitempty"`
Send uint32 `json:"send,omitempty"`
}
func Between(s, sub1, sub2 string) string {
i := strings.Index(s, sub1)
+73 -19
View File
@@ -1,6 +1,7 @@
package streamer
import (
"encoding/json"
"fmt"
"github.com/pion/sdp/v3"
"strconv"
@@ -32,9 +33,12 @@ const (
CodecAAC = "MPEG4-GENERIC"
CodecOpus = "OPUS" // payloadType: 111
CodecG722 = "G722"
CodecMPA = "MPA" // payload: 14
CodecMP3 = "MPA" // payload: 14, aka MPEG-1 Layer III
CodecELD = "ELD" // AAC-ELD
CodecAll = "ALL"
CodecAny = "ANY"
)
const PayloadTypeRAW byte = 255
@@ -43,7 +47,7 @@ func GetKind(name string) string {
switch name {
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG:
return KindVideo
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMPA, CodecELD:
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecELD:
return KindAudio
}
return ""
@@ -59,7 +63,6 @@ type Media struct {
MID string `json:"mid,omitempty"` // TODO: fixme?
Control string `json:"control,omitempty"` // TODO: fixme?
Title string `json:"title,omitempty"` // TODO: fixme?
}
func (m *Media) String() string {
@@ -70,6 +73,10 @@ func (m *Media) String() string {
return s
}
func (m *Media) MarshalJSON() ([]byte, error) {
return json.Marshal(m.String())
}
func (m *Media) Clone() *Media {
clone := *m
return &clone
@@ -107,10 +114,6 @@ func (m *Media) MatchMedia(media *Media) *Codec {
}
for _, localCodec := range m.Codecs {
if media.Codecs == nil {
return localCodec
}
for _, remoteCodec := range media.Codecs {
if localCodec.Match(remoteCodec) {
return localCodec
@@ -120,6 +123,10 @@ func (m *Media) MatchMedia(media *Media) *Codec {
return nil
}
func (m *Media) MatchAll() bool {
return len(m.Codecs) > 0 && m.Codecs[0].Name == CodecAll
}
// Codec take best from:
// - deepch/vdk/av.CodecData
// - pion/webrtc.RTPCodecCapability
@@ -149,19 +156,18 @@ func (c *Codec) Clone() *Codec {
}
func (c *Codec) Match(codec *Codec) bool {
switch codec.Name {
case CodecAll, CodecAny:
return true
}
return c.Name == codec.Name &&
(c.ClockRate == codec.ClockRate || codec.ClockRate == 0) &&
(c.Channels == codec.Channels || codec.Channels == 0)
}
func UnmarshalSDP(rawSDP []byte) ([]*Media, error) {
sd := &sdp.SessionDescription{}
if err := sd.Unmarshal(rawSDP); err != nil {
return nil, err
}
var medias []*Media
for _, md := range sd.MediaDescriptions {
func UnmarshalMedias(descriptions []*sdp.MediaDescription) (medias []*Media) {
for _, md := range descriptions {
media := UnmarshalMedia(md)
if media.Direction == DirectionSendRecv {
@@ -175,11 +181,25 @@ func UnmarshalSDP(rawSDP []byte) ([]*Media, error) {
medias = append(medias, media)
}
return medias, nil
return
}
func MarshalSDP(medias []*Media) ([]byte, error) {
sd := &sdp.SessionDescription{}
func MarshalSDP(name string, medias []*Media) ([]byte, error) {
sd := &sdp.SessionDescription{
Origin: sdp.Origin{
Username: "-", SessionID: 1, SessionVersion: 1,
NetworkType: "IN", AddressType: "IP4", UnicastAddress: "0.0.0.0",
},
SessionName: sdp.SessionName(name),
ConnectionInformation: &sdp.ConnectionInformation{
NetworkType: "IN", AddressType: "IP4", Address: &sdp.Address{
Address: "0.0.0.0",
},
},
TimeDescriptions: []sdp.TimeDescription{
{Timing: sdp.Timing{}},
},
}
payloadType := uint8(96)
@@ -267,7 +287,7 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
c.Name = CodecPCMA
c.ClockRate = 8000
case "14":
c.Name = CodecMPA
c.Name = CodecMP3
c.ClockRate = 44100
case "26":
c.Name = CodecJPEG
@@ -280,6 +300,40 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
return c
}
func ParseQuery(query map[string][]string) (medias []*Media) {
// set media candidates from query list
for key, values := range query {
switch key {
case KindVideo, KindAudio:
for _, value := range values {
media := &Media{Kind: key, Direction: DirectionRecvonly}
for _, name := range strings.Split(value, ",") {
name = strings.ToUpper(name)
// check aliases
switch name {
case "", "COPY":
name = CodecAny
case "MJPEG":
name = CodecJPEG
case "AAC":
name = CodecAAC
case "MP3":
name = CodecMP3
}
media.Codecs = append(media.Codecs, &Codec{Name: name})
}
medias = append(medias, media)
}
}
}
return
}
func atoi(s string) (i int) {
i, _ = strconv.Atoi(s)
return
+42
View File
@@ -0,0 +1,42 @@
package streamer
import (
"github.com/pion/sdp/v3"
"github.com/stretchr/testify/assert"
"net/url"
"testing"
)
func TestSDP(t *testing.T) {
medias := []*Media{{
Kind: KindAudio, Direction: DirectionSendonly,
Codecs: []*Codec{
{Name: CodecPCMU, ClockRate: 8000},
},
}}
data, err := MarshalSDP("go2rtc/1.0.0", medias)
assert.Empty(t, err)
sd := &sdp.SessionDescription{}
err = sd.Unmarshal(data)
assert.Empty(t, err)
}
func TestParseQuery(t *testing.T) {
u, _ := url.Parse("rtsp://localhost:8554/camera1")
medias := ParseQuery(u.Query())
assert.Nil(t, medias)
for _, rawULR := range []string{
"rtsp://localhost:8554/camera1?video",
"rtsp://localhost:8554/camera1?video=copy",
"rtsp://localhost:8554/camera1?video=any",
} {
u, _ = url.Parse(rawULR)
medias = ParseQuery(u.Query())
assert.Equal(t, []*Media{
{Kind: KindVideo, Direction: DirectionRecvonly, Codecs: []*Codec{{Name: CodecAny}}},
}, medias)
}
}
+11 -3
View File
@@ -1,6 +1,7 @@
package streamer
import (
"encoding/json"
"fmt"
"github.com/pion/rtp"
"sync"
@@ -22,12 +23,19 @@ func NewTrack(codec *Codec, direction string) *Track {
func (t *Track) String() string {
s := t.Codec.String()
t.sinkMu.RLock()
s += fmt.Sprintf(", sinks=%d", len(t.sink))
t.sinkMu.RUnlock()
if t.sinkMu.TryRLock() {
s += fmt.Sprintf(", sinks=%d", len(t.sink))
t.sinkMu.RUnlock()
} else {
s += fmt.Sprintf(", sinks=?")
}
return s
}
func (t *Track) MarshalJSON() ([]byte, error) {
return json.Marshal(t.String())
}
func (t *Track) WriteRTP(p *rtp.Packet) error {
t.sinkMu.RLock()
for _, f := range t.sink {
+3 -1
View File
@@ -70,7 +70,9 @@ func (a *Auth) Write(req *Request) {
case AuthBasic:
req.Header.Set("Authorization", a.header)
case AuthDigest:
uri := req.URL.RequestURI()
// important to use String except RequestURL for RtspServer:
// https://github.com/AlexxIT/go2rtc/issues/244
uri := req.URL.String()
h2 := HexMD5(req.Method, uri)
response := HexMD5(a.h1nonce, h2)
header := a.header + fmt.Sprintf(
+194
View File
@@ -0,0 +1,194 @@
package ts
import (
"bytes"
"encoding/hex"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/aacparser"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/ts"
"github.com/pion/rtp"
"sync/atomic"
"time"
)
type Consumer struct {
streamer.Element
UserAgent string
RemoteAddr string
buf *bytes.Buffer
muxer *ts.Muxer
mimeType string
streams []av.CodecData
start bool
init []byte
send uint32
}
func (c *Consumer) GetMedias() []*streamer.Media {
return []*streamer.Media{
{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecH264},
},
},
//{
// Kind: streamer.KindAudio,
// Direction: streamer.DirectionRecvonly,
// Codecs: []*streamer.Codec{
// {Name: streamer.CodecAAC},
// },
//},
}
}
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
codec := track.Codec
trackID := int8(len(c.streams))
switch codec.Name {
case streamer.CodecH264:
sps, pps := h264.GetParameterSet(codec.FmtpLine)
stream, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
if err != nil {
return nil
}
if len(c.mimeType) > 0 {
c.mimeType += ","
}
c.mimeType += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
c.streams = append(c.streams, stream)
pkt := av.Packet{Idx: trackID, CompositionTime: time.Millisecond}
ts2time := time.Second / time.Duration(codec.ClockRate)
push := func(packet *rtp.Packet) error {
if packet.Version != h264.RTPPacketVersionAVC {
return nil
}
if !c.start {
return nil
}
pkt.Data = packet.Payload
newTime := time.Duration(packet.Timestamp) * ts2time
if pkt.Time > 0 {
pkt.Duration = newTime - pkt.Time
}
pkt.Time = newTime
if err = c.muxer.WritePacket(pkt); err != nil {
return err
}
// clone bytes from buffer, so next packet won't overwrite it
buf := append([]byte{}, c.buf.Bytes()...)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
c.buf.Reset()
return nil
}
if codec.IsRTP() {
wrapper := h264.RTPDepay(track)
push = wrapper(push)
}
return track.Bind(push)
case streamer.CodecAAC:
s := streamer.Between(codec.FmtpLine, "config=", ";")
b, err := hex.DecodeString(s)
if err != nil {
return nil
}
stream, err := aacparser.NewCodecDataFromMPEG4AudioConfigBytes(b)
if err != nil {
return nil
}
if len(c.mimeType) > 0 {
c.mimeType += ","
}
c.mimeType += "mp4a.40.2"
c.streams = append(c.streams, stream)
pkt := av.Packet{Idx: trackID, CompositionTime: time.Millisecond}
ts2time := time.Second / time.Duration(codec.ClockRate)
push := func(packet *rtp.Packet) error {
if !c.start {
return nil
}
pkt.Data = packet.Payload
newTime := time.Duration(packet.Timestamp) * ts2time
if pkt.Time > 0 {
pkt.Duration = newTime - pkt.Time
}
pkt.Time = newTime
if err := c.muxer.WritePacket(pkt); err != nil {
return err
}
// clone bytes from buffer, so next packet won't overwrite it
buf := append([]byte{}, c.buf.Bytes()...)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
c.buf.Reset()
return nil
}
if codec.IsRTP() {
wrapper := aac.RTPDepay(track)
push = wrapper(push)
}
return track.Bind(push)
}
panic("unsupported codec")
}
func (c *Consumer) MimeCodecs() string {
return c.mimeType
}
func (c *Consumer) Init() ([]byte, error) {
c.buf = bytes.NewBuffer(nil)
c.muxer = ts.NewMuxer(c.buf)
// first packet will be with header, it's ok
if err := c.muxer.WriteHeader(c.streams); err != nil {
return nil, err
}
data := append([]byte{}, c.buf.Bytes()...)
return data, nil
}
func (c *Consumer) Start() {
c.start = true
}
+10 -6
View File
@@ -35,13 +35,17 @@ func NewAPI(address string) (*webrtc.API, error) {
s.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
if address != "" {
ln, err := net.Listen("tcp", address)
if err == nil {
s.SetNetworkTypes([]webrtc.NetworkType{
webrtc.NetworkTypeUDP4, webrtc.NetworkTypeUDP6,
webrtc.NetworkTypeTCP4, webrtc.NetworkTypeTCP6,
})
s.SetNetworkTypes([]webrtc.NetworkType{
webrtc.NetworkTypeUDP4, webrtc.NetworkTypeUDP6,
webrtc.NetworkTypeTCP4, webrtc.NetworkTypeTCP6,
})
if ln, err := net.ListenPacket("udp", address); err == nil {
udpMux := webrtc.NewICEUDPMux(nil, ln)
s.SetICEUDPMux(udpMux)
}
if ln, err := net.Listen("tcp", address); err == nil {
tcpMux := webrtc.NewICETCPMux(nil, ln, 8)
s.SetICETCPMux(tcpMux)
}
+6 -2
View File
@@ -2,6 +2,7 @@ package webrtc
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/sdp/v3"
"github.com/pion/webrtc/v3"
)
@@ -90,12 +91,15 @@ func (c *Conn) SetOffer(offer string) (err error) {
if err = c.Conn.SetRemoteDescription(sdOffer); err != nil {
return
}
rawSDP := []byte(c.Conn.RemoteDescription().SDP)
medias, err := streamer.UnmarshalSDP(rawSDP)
if err != nil {
sd := &sdp.SessionDescription{}
if err = sd.Unmarshal(rawSDP); err != nil {
return
}
medias := streamer.UnmarshalMedias(sd.MediaDescriptions)
// sort medias, so video will always be before audio
// and ignore application media from Hass default lovelace card
for _, media := range medias {
+7 -15
View File
@@ -113,20 +113,12 @@ func (c *Conn) AddCandidate(candidate string) {
}
func (c *Conn) MarshalJSON() ([]byte, error) {
v := map[string]interface{}{
streamer.JSONType: "WebRTC server consumer",
streamer.JSONRemoteAddr: c.remote(),
info := &streamer.Info{
Type: "WebRTC client",
RemoteAddr: c.remote(),
UserAgent: c.UserAgent,
Recv: uint32(c.receive),
Send: uint32(c.send),
}
if c.receive > 0 {
v[streamer.JSONReceive] = c.receive
}
if c.send > 0 {
v[streamer.JSONSend] = c.send
}
if c.UserAgent != "" {
v[streamer.JSONUserAgent] = c.UserAgent
}
return json.Marshal(v)
return json.Marshal(info)
}
+15 -8
View File
@@ -1,6 +1,7 @@
package webrtc
import (
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/ice/v2"
@@ -12,24 +13,30 @@ import (
"time"
)
func NewCandidate(address string) (string, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return "", err
func NewCandidate(network, address string) (string, error) {
i := strings.LastIndexByte(address, ':')
if i < 0 {
return "", errors.New("wrong candidate: " + address)
}
host, port := address[:i], address[i+1:]
i, err := strconv.Atoi(port)
if err != nil {
return "", err
}
cand, err := ice.NewCandidateHost(&ice.CandidateHostConfig{
Network: "tcp",
config := &ice.CandidateHostConfig{
Network: network,
Address: host,
Port: i,
Component: ice.ComponentRTP,
TCPType: ice.TCPTypePassive,
})
}
if network == "tcp" {
config.TCPType = ice.TCPTypePassive
}
cand, err := ice.NewCandidateHost(config)
if err != nil {
return "", err
}
+5 -5
View File
@@ -3,17 +3,17 @@
@SET GOOS=windows
@SET GOARCH=amd64
@SET FILENAME=go2rtc_win64.zip
go build -ldflags "-s -w" -trimpath && 7z a -sdel %FILENAME% go2rtc.exe
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc.exe
@SET GOOS=windows
@SET GOARCH=386
@SET FILENAME=go2rtc_win32.zip
go build -ldflags "-s -w" -trimpath && 7z a -sdel %FILENAME% go2rtc.exe
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc.exe
@SET GOOS=windows
@SET GOARCH=arm64
@SET FILENAME=go2rtc_win_arm64.zip
go build -ldflags "-s -w" -trimpath && 7z a -sdel %FILENAME% go2rtc.exe
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc.exe
@SET GOOS=linux
@SET GOARCH=amd64
@@ -44,9 +44,9 @@ go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME%
@SET GOOS=darwin
@SET GOARCH=amd64
@SET FILENAME=go2rtc_mac_amd64.zip
go build -ldflags "-s -w" -trimpath && 7z a -sdel %FILENAME% go2rtc
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc
@SET GOOS=darwin
@SET GOARCH=arm64
@SET FILENAME=go2rtc_mac_arm64.zip
go build -ldflags "-s -w" -trimpath && 7z a -sdel %FILENAME% go2rtc
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc
+15
View File
@@ -48,6 +48,18 @@ pc.ontrack = ev => {
}
```
## Chromecast 1
2023-02-02. Error:
```
InvalidStateError: Failed to execute 'addTransceiver' on 'RTCPeerConnection': This operation is only supported in 'unified-plan'. 'unified-plan' will become the default behavior in the future, but it is currently experimental. To try it out, construct the RTCPeerConnection with sdpSemantics:'unified-plan' present in the RTCConfiguration argument.
```
User-Agent: `Mozilla/5.0 (X11; Linux armv7l) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.47 Safari/537.36 CrKey/1.36.159268`
https://webrtc.org/getting-started/unified-plan-transition-guide?hl=en
## Useful links
- https://www.webrtc-experiment.com/DetectRTC/
@@ -56,3 +68,6 @@ pc.ontrack = ev => {
- https://web.dev/i18n/en/fast-playback-with-preload/#manual_buffering
- https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API
- https://chromium.googlesource.com/external/w3c/web-platform-tests/+/refs/heads/master/media-source/mediasource-is-type-supported.html
- https://googlechrome.github.io/samples/media/sourcebuffer-changetype.html
- https://chromestatus.com/feature/5100845653819392
- https://developer.apple.com/documentation/webkit/delivering_video_content_for_safari
+1 -1
View File
@@ -53,7 +53,7 @@
const video = document.createElement("video");
out.innerText += "video.canPlayType\n";
types.forEach(type => {
out.innerText += type + "=" + (video.canPlayType(type) ? "true" : "false") + "\n";
out.innerText += `${type} = ${MediaSource.isTypeSupported(type)} / ${video.canPlayType(type)}\n`;
})
</script>
+1 -1
View File
@@ -58,7 +58,7 @@
0, location.pathname.lastIndexOf("/")
);
fetch(`${baseUrl}/api/devices`)
fetch(`${baseUrl}/api/devices`, {cache: 'no-cache'})
.then(r => r.json())
.then(data => {
document.querySelector("body > table > tbody").innerHTML =
+69
View File
@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html>
<head>
<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.14.0/ace.min.js"></script>
<style>
body {
font-family: Arial, Helvetica, sans-serif;
}
body {
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
html, body, #config {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<script src="main.js"></script>
<div>
<button id="save">Save & Restart</button>
</div>
<br>
<div id="config"></div>
<script>
ace.config.set('basePath', 'https://cdnjs.cloudflare.com/ajax/libs/ace/1.14.0/');
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', {method: 'POST'});
} else {
r.text().then(alert);
}
});
});
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})`);
}
});
})
</script>
</body>
</html>
+1 -1
View File
@@ -65,7 +65,7 @@
0, location.pathname.lastIndexOf("/")
);
fetch(`${baseUrl}/api/homekit`)
fetch(`${baseUrl}/api/homekit`, {cache: 'no-cache'})
.then(r => r.json())
.then(data => {
document.querySelector("body > table > tbody").innerHTML =
+15 -8
View File
@@ -10,6 +10,7 @@
<style>
body {
font-family: Arial, Helvetica, sans-serif;
background-color: white;
}
table {
@@ -61,6 +62,7 @@
</head>
<body>
<script src="main.js"></script>
<div class="info"></div>
<div class="header">
<input id="src" type="text" placeholder="url">
<a id="add" href="#">add</a>
@@ -87,10 +89,7 @@
const templates = [
'<a href="stream.html?src={name}">stream</a>',
'<a href="webrtc.html?src={name}">2-way-aud</a>',
'<a href="api/stream.mp4?src={name}">mp4</a>',
'<a href="api/stream.mjpeg?src={name}">mjpeg</a>',
`<a href="rtsp://${location.hostname}:8554/{name}">rtsp</a>`,
'<a href="api/streams?src={name}">info</a>',
'<a href="links.html?src={name}">links</a>',
'<a href="#" data-name="{name}">delete</a>',
];
@@ -132,25 +131,33 @@
function reload() {
const url = new URL("api/streams", location.href);
fetch(url).then(r => r.json()).then(data => {
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {
tbody.innerHTML = "";
for (const [name, value] of Object.entries(data)) {
const online = value ? value.length : 0;
const online = value && value.consumers ? value.consumers.length : 0;
const src = encodeURIComponent(name);
const links = templates.map(link => {
return link.replace("{name}", encodeURIComponent(name));
return link.replace("{name}", src);
}).join(" ");
const tr = document.createElement("tr");
tr.dataset["id"] = name;
tr.innerHTML =
`<td><label><input type="checkbox" name="${name}">${name}</label></td>` +
`<td>${online}</td><td>${links}</td>`;
`<td><a href="api/streams?src=${src}">${online} / info</a></td>` +
`<td>${links}</td>`;
tbody.appendChild(tr);
}
});
}
const url = new URL("api", location.href);
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {
const info = document.querySelector(".info");
info.innerText = `Version: ${data.version}, Config: ${data.config_path}`;
});
reload();
</script>
</body>
+89
View File
@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html>
<head>
<title>go2rtc - links</title>
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<style>
body {
font-family: Arial, Helvetica, sans-serif;
}
body {
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
html, body, #config {
width: 100%;
height: 100%;
}
div {
padding: 10px;
}
div > li {
list-style-type: none;
padding-left: 10px;
position: relative;
}
div > li:before {
content: "-";
position: absolute;
left: 0;
}
</style>
</head>
<body>
<script src="main.js"></script>
<div id="links"></div>
<script>
const params = new URLSearchParams(location.search);
const src = params.get("src");
const links = document.querySelector("#links");
links.innerHTML = `
<h2>Any codec in source</h2>
<li><a href="stream.html?src=${src}">stream.html</a> with auto-select mode / browsers: all / codecs: H264, H265*, MJPEG, JPEG, AAC, PCMU, PCMA, OPUS</li>
<li><a href="api/streams?src=${src}">info.json</a> page with active connections</li>
`;
const url = new URL('api', location.href);
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {
let rtsp = location.host + ':8554';
try {
const host = data.host.match(/^[^:]+/)[0];
const port = data.rtsp.listen.match(/[0-9]+$/)[0];
rtsp = `${host}:${port}`;
} catch (e) {
}
const links = document.querySelector("#links");
links.innerHTML += `
<li><a href="rtsp://${rtsp}/${src}">rtsp</a> with only one video and one audio / codecs: any</li>
<li><a href="rtsp://${rtsp}/${src}?mp4">rtsp</a> for MP4 recording (Hass or Frigate) / codecs: H264, H265, AAC</li>
<li><a href="rtsp://${rtsp}/${src}?video=all&audio=all">rtsp</a> with all tracks / codecs: any</li>
<h2>H264/H265 source</h2>
<li><a href="stream.html?src=${src}&mode=webrtc">stream.html</a> WebRTC stream / browsers: all / codecs: H264, PCMU, PCMA, OPUS / +H265 in Safari</li>
<li><a href="stream.html?src=${src}&mode=mse">stream.html</a> MSE stream / browsers: Chrome, Firefox, Safari Mac/iPad / codecs: H264, H265*, AAC / +OPUS in Chrome and Firefox</li>
<li><a href="api/stream.mp4?src=${src}">stream.mp4</a> MP4 stream with AAC audio / browsers: Chrome, Firefox / codecs: H264, H265*, AAC</li>
<li><a href="api/stream.mp4?src=${src}&video=h264,h265&audio=aac,opus,mp3,pcma,pcmu">stream.mp4</a> MP4 stream with any audio / browsers: Chrome / codecs: H264, H265*, AAC, OPUS, MP3, PCMU, PCMA</li>
<li><a href="api/frame.mp4?src=${src}">frame.mp4</a> snapshot in MP4-format / browsers: all / codecs: H264, H265*</li>
<li><a href="api/stream.m3u8?src=${src}">stream.m3u8</a> HLS/TS / browsers: Safari all, Chrome Android / codecs: H264</li>
<li><a href="api/stream.m3u8?src=${src}&mp4">stream.m3u8</a> HLS/fMP4 / browsers: Safari all, Chrome Android / codecs: H264, H265*, AAC</li>
<li><a href="webrtc.html?src=${src}">webrtc.html</a> with two-way audio for supported cameras / browsers: all / codecs: H264, PCMU, PCMA, OPUS</li>
<h2>MJPEG source</h2>
<li><a href="stream.html?src=${src}&mode=mjpeg">stream.html</a> with MJPEG mode / browsers: all / codecs: MJPEG, JPEG</li>
<li><a href="api/stream.mjpeg?src=${src}">stream.mjpeg</a> MJPEG stream / browsers: all / codecs: MJPEG, JPEG</li>
<li><a href="api/frame.jpeg?src=${src}">frame.jpeg</a> snapshot in JPEG-format / browsers: all / codecs: MJPEG, JPEG</li>
`;
});
</script>
</body>
</html>
+1
View File
@@ -47,6 +47,7 @@ nav li {
<li><a href="index.html">Streams</a></li>
<li><a href="devices.html">Devices</a></li>
<li><a href="homekit.html">HomeKit</a></li>
<li><a href="editor.html">Config</a></li>
</ul>
</nav>
` + document.body.innerHTML;
+7 -6
View File
@@ -3,13 +3,13 @@
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>go2rtc - Stream</title>
<script src="video-rtc.js"></script>
<style>
body {
background: black;
margin: 0;
padding: 0;
display: flex;
font-family: Arial, Helvetica, sans-serif;
}
html, body {
@@ -25,7 +25,8 @@
</style>
</head>
<body>
<script>
<script type="module" src="./video-stream.js"></script>
<script type="module">
const params = new URLSearchParams(location.search);
// support multiple streams and multiple modes
@@ -44,16 +45,16 @@
document.body.className = "flex";
}
const background = params.get("background") === "true";
const background = params.get("background") !== "false";
const width = "1 0 " + (params.get("width") || "320px");
for (let i = 0; i < streams.length; i++) {
/** @type {VideoRTC} */
const video = document.createElement("video-rtc");
/** @type {VideoStream} */
const video = document.createElement("video-stream");
video.background = background;
video.mode = modes[i] || video.mode;
video.style.flex = width;
video.src = new URL("api/ws?src=" + streams[i], location.href);
video.src = new URL("api/ws?src=" + encodeURIComponent(streams[i]), location.href);
document.body.appendChild(video);
}
</script>
+264 -251
View File
@@ -1,5 +1,5 @@
/**
* Video player for MSE and WebRTC connections.
* Video player for go2rtc streaming application.
*
* All modern web technologies are supported in almost any browser except Apple Safari.
*
@@ -11,8 +11,9 @@
* - MediaSource for Safari iOS all
* - Customized built-in elements (extends HTMLVideoElement) because all Safari
* - Public class fields because old Safari (before 14.0)
* - Autoplay for Safari
*/
class VideoRTC extends HTMLElement {
export class VideoRTC extends HTMLElement {
constructor() {
super();
@@ -26,37 +27,57 @@ class VideoRTC extends HTMLElement {
"hvc1.1.6.L153.B0", // H.265 main 5.1 (Chromecast Ultra)
"mp4a.40.2", // AAC LC
"mp4a.40.5", // AAC HE
"mp4a.69", // MP3
"mp4a.6B", // MP3
"opus", // OPUS Chrome
];
/**
* Supported modes (webrtc, mse, mp4, mjpeg).
* [config] Supported modes (webrtc, mse, mp4, mjpeg).
* @type {string}
*/
this.mode = "webrtc,mse,mp4,mjpeg";
/**
* Run stream when not displayed on the screen. Default `false`.
* [config] Run stream when not displayed on the screen. Default `false`.
* @type {boolean}
*/
this.background = false;
/**
* Run stream only when player in the viewport. Stop when user scroll out player.
* [config] Run stream only when player in the viewport. Stop when user scroll out player.
* Value is percentage of visibility from `0` (not visible) to `1` (full visible).
* Default `0` - disable;
* @type {number}
*/
this.intersectionThreshold = 0;
this.visibilityThreshold = 0;
/**
* Run stream only when browser page on the screen. Stop when user change browser
* [config] Run stream only when browser page on the screen. Stop when user change browser
* tab or minimise browser windows.
* @type {boolean}
*/
this.visibilityCheck = true;
/**
* [config] WebRTC configuration
* @type {RTCConfiguration}
*/
this.pcConfig = {
iceServers: [{urls: 'stun:stun.l.google.com:19302'}],
sdpSemantics: 'unified-plan', // important for Chromecast 1
};
/**
* [info] WebSocket connection state. Values: CONNECTING, OPEN, CLOSED
* @type {number}
*/
this.wsState = WebSocket.CLOSED;
/**
* [info] WebRTC connection state.
* @type {number}
*/
this.pcState = WebSocket.CLOSED;
/**
* @type {HTMLVideoElement}
*/
@@ -68,16 +89,9 @@ class VideoRTC extends HTMLElement {
this.ws = null;
/**
* Internal WebSocket connection state. Values: CONNECTING, OPEN, CLOSED
* @type {number}
*/
this.wsState = WebSocket.CLOSED;
/**
* Internal WebSocket URL.
* @type {string|URL}
*/
this.url = "";
this.wsURL = "";
/**
* @type {RTCPeerConnection}
@@ -87,37 +101,38 @@ class VideoRTC extends HTMLElement {
/**
* @type {number}
*/
this.pcState = WebSocket.CLOSED;
this.pcConfig = {iceServers: [{urls: "stun:stun.l.google.com:19302"}]};
this.connectTS = 0;
/**
* Internal disconnect TimeoutID.
* @type {string}
*/
this.mseCodecs = "";
/**
* [internal] Disconnect TimeoutID.
* @type {number}
*/
this.disconnectTimeout = 0;
this.disconnectTID = 0;
/**
* Internal reconnect TimeoutID.
* [internal] Reconnect TimeoutID.
* @type {number}
*/
this.reconnectTimeout = 0;
this.reconnectTID = 0;
/**
* Handler for receiving Binary from WebSocket
* [internal] Handler for receiving Binary from WebSocket.
* @type {Function}
*/
this.ondata = null;
/**
* Handlers list for receiving JSON from WebSocket
* [internal] Handlers list for receiving JSON from WebSocket
* @type {Object.<string,Function>}}
*/
this.onmessage = null;
}
/** public properties **/
/**
* Set video source (WebSocket URL). Support relative path.
* @param {string|URL} value
@@ -130,9 +145,9 @@ class VideoRTC extends HTMLElement {
value = "ws" + location.origin.substring(4) + value;
}
this.url = value;
this.wsURL = value;
if (this.isConnected) this.connectedCallback();
this.onconnect();
}
/**
@@ -156,10 +171,6 @@ class VideoRTC extends HTMLElement {
if (this.ws) this.ws.send(JSON.stringify(value));
}
get closed() {
return this.wsState === WebSocket.CLOSED && this.pcState === WebSocket.CLOSED;
}
codecs(type) {
const test = type === "mse"
? codec => MediaSource.isTypeSupported(`video/mp4; codecs="${codec}"`)
@@ -172,11 +183,9 @@ class VideoRTC extends HTMLElement {
* document-connected element.
*/
connectedCallback() {
console.debug("VideoRTC.connectedCallback", this.wsState, this.pcState);
if (this.disconnectTimeout) {
clearTimeout(this.disconnectTimeout);
this.disconnectTimeout = 0;
if (this.disconnectTID) {
clearTimeout(this.disconnectTID);
this.disconnectTID = 0;
}
// because video autopause on disconnected from DOM
@@ -184,17 +193,13 @@ class VideoRTC extends HTMLElement {
const seek = this.video.seekable;
if (seek.length > 0) {
this.video.currentTime = seek.end(seek.length - 1);
this.play();
}
this.play();
} else {
this.oninit();
}
if (!this.url || !this.closed) return;
// CLOSED => CONNECTING
this.wsState = WebSocket.CONNECTING;
this.internalInit();
this.internalWS();
this.onconnect();
}
/**
@@ -202,50 +207,36 @@ class VideoRTC extends HTMLElement {
* document's DOM.
*/
disconnectedCallback() {
console.debug("VideoRTC.disconnectedCallback", this.wsState, this.pcState);
if (this.background || this.disconnectTID) return;
if (this.wsState === WebSocket.CLOSED && this.pcState === WebSocket.CLOSED) return;
if (this.background || this.disconnectTimeout || this.closed) return;
this.disconnectTimeout = setTimeout(() => {
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = 0;
this.disconnectTID = setTimeout(() => {
if (this.reconnectTID) {
clearTimeout(this.reconnectTID);
this.reconnectTID = 0;
}
this.disconnectTimeout = 0;
this.disconnectTID = 0;
this.wsState = WebSocket.CLOSED;
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.pcState = WebSocket.CLOSED;
if (this.pc) {
this.pc.close();
this.pc = null;
}
this.ondisconnect();
}, this.DISCONNECT_TIMEOUT);
}
internalInit() {
if (this.childElementCount) return;
/**
* Creates child DOM elements. Called automatically once on `connectedCallback`.
*/
oninit() {
this.video = document.createElement("video");
this.video.controls = true;
this.video.playsInline = true;
this.video.preload = "auto";
this.appendChild(this.video);
// important for second video for mode MP4
this.style.display = "block";
this.style.position = "relative";
this.video.style.display = "block"; // fix bottom margin 4px
this.video.style.width = "100%";
this.video.style.height = "100%"
this.appendChild(this.video);
if (this.background) return;
if ("hidden" in document && this.visibilityCheck) {
@@ -258,7 +249,7 @@ class VideoRTC extends HTMLElement {
})
}
if ("IntersectionObserver" in window && this.intersectionThreshold) {
if ("IntersectionObserver" in window && this.visibilityThreshold) {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (!entry.isIntersecting) {
@@ -267,92 +258,120 @@ class VideoRTC extends HTMLElement {
this.connectedCallback();
}
});
}, {threshold: this.intersectionThreshold});
}, {threshold: this.visibilityThreshold});
observer.observe(this);
}
}
internalWS() {
if (this.wsState !== WebSocket.CONNECTING) return;
if (this.ws) throw "connect with non null WebSocket";
/**
* Connect to WebSocket. Called automatically on `connectedCallback`.
* @return {boolean} true if the connection has started.
*/
onconnect() {
if (!this.isConnected || !this.wsURL || this.ws || this.pc) return false;
const ts = Date.now();
// CLOSED or CONNECTING => CONNECTING
this.wsState = WebSocket.CONNECTING;
this.ws = new WebSocket(this.url);
this.connectTS = Date.now();
this.ws = new WebSocket(this.wsURL);
this.ws.binaryType = "arraybuffer";
this.ws.addEventListener("open", ev => this.onopen(ev));
this.ws.addEventListener("close", ev => this.onclose(ev));
this.ws.addEventListener("open", () => {
console.debug("VideoRTC.ws.open", this.wsState);
// CONNECTING => OPEN
this.wsState = WebSocket.OPEN;
this.ws.addEventListener("message", ev => {
if (typeof ev.data === "string") {
const msg = JSON.parse(ev.data);
for (const mode in this.onmessage) {
this.onmessage[mode](msg);
}
} else {
this.ondata(ev.data);
}
});
this.ondata = null;
this.onmessage = {};
let firstMode = "";
if (this.mode.indexOf("mse") >= 0 && "MediaSource" in window) { // iPhone
firstMode ||= "mse";
this.internalMSE();
} else if (this.mode.indexOf("mp4") >= 0) {
firstMode ||= "mp4";
this.internalMP4();
}
if (this.mode.indexOf("webrtc") >= 0 && "RTCPeerConnection" in window) { // macOS Desktop app
firstMode ||= "webrtc";
this.internalRTC();
}
if (this.mode.indexOf("mjpeg") >= 0) {
if (firstMode) {
this.onmessage["mjpeg"] = msg => {
if (msg.type !== "error" || msg.value.indexOf(firstMode) !== 0) return;
this.internalMJPEG();
}
} else {
this.internalMJPEG();
}
}
});
this.ws.addEventListener("close", () => {
console.debug("VideoRTC.ws.close", this.wsState);
if (this.wsState === WebSocket.CLOSED) return;
// CONNECTING, OPEN => CONNECTING
this.wsState = WebSocket.CONNECTING;
this.ws = null;
// reconnect no more than once every X seconds
const delay = Math.max(this.RECONNECT_TIMEOUT - (Date.now() - ts), 0);
this.reconnectTimeout = setTimeout(() => {
this.reconnectTimeout = 0;
this.internalWS();
}, delay);
});
return true;
}
internalMSE() {
console.debug("VideoRTC.internalMSE");
ondisconnect() {
this.wsState = WebSocket.CLOSED;
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.pcState = WebSocket.CLOSED;
if (this.pc) {
this.pc.close();
this.pc = null;
}
}
/**
* @returns {Array.<string>} of modes (mse, webrtc, etc.)
*/
onopen() {
// CONNECTING => OPEN
this.wsState = WebSocket.OPEN;
this.ws.addEventListener("message", ev => {
if (typeof ev.data === "string") {
const msg = JSON.parse(ev.data);
for (const mode in this.onmessage) {
this.onmessage[mode](msg);
}
} else {
this.ondata(ev.data);
}
});
this.ondata = null;
this.onmessage = {};
const modes = [];
if (this.mode.indexOf("mse") >= 0 && "MediaSource" in window) { // iPhone
modes.push("mse");
this.onmse();
} else if (this.mode.indexOf("mp4") >= 0) {
modes.push("mp4");
this.onmp4();
}
if (this.mode.indexOf("webrtc") >= 0 && "RTCPeerConnection" in window) { // macOS Desktop app
modes.push("webrtc");
this.onwebrtc();
}
if (this.mode.indexOf("mjpeg") >= 0) {
if (modes.length) {
this.onmessage["mjpeg"] = msg => {
if (msg.type !== "error" || msg.value.indexOf(modes[0]) !== 0) return;
this.onmjpeg();
}
} else {
modes.push("mjpeg");
this.onmjpeg();
}
}
return modes;
}
/**
* @return {boolean} true if reconnection has started.
*/
onclose() {
if (this.wsState === WebSocket.CLOSED) return false;
// CONNECTING, OPEN => CONNECTING
this.wsState = WebSocket.CONNECTING;
this.ws = null;
// reconnect no more than once every X seconds
const delay = Math.max(this.RECONNECT_TIMEOUT - (Date.now() - this.connectTS), 0);
this.reconnectTID = setTimeout(() => {
this.reconnectTID = 0;
this.onconnect();
}, delay);
return true;
}
onmse() {
const ms = new MediaSource();
ms.addEventListener("sourceopen", () => {
console.debug("VideoRTC.ms.sourceopen");
URL.revokeObjectURL(this.video.src);
this.send({type: "mse", value: this.codecs("mse")});
}, {once: true});
@@ -361,28 +380,34 @@ class VideoRTC extends HTMLElement {
this.video.srcObject = null;
this.play();
this.mseCodecs = "";
this.onmessage["mse"] = msg => {
if (msg.type !== "mse") return;
this.mseCodecs = msg.value;
const sb = ms.addSourceBuffer(msg.value);
sb.mode = "segments"; // segments or sequence
sb.addEventListener("updateend", () => {
if (sb.updating) return;
if (bufLen > 0) {
try {
sb.appendBuffer(buf.slice(0, bufLen));
} catch (e) {
console.debug(e);
try {
if (bufLen > 0) {
const data = buf.slice(0, bufLen);
bufLen = 0;
sb.appendBuffer(data);
} else if (sb.buffered && sb.buffered.length) {
const end = sb.buffered.end(sb.buffered.length - 1) - 15;
const start = sb.buffered.start(0);
if (end > start) {
sb.remove(start, end);
ms.setLiveSeekableRange(end, end + 15);
}
// console.debug("VideoRTC.buffered", start, end);
}
bufLen = 0;
} else if (sb.buffered.length) {
const end = sb.buffered.end(sb.buffered.length - 1) - 5;
const start = sb.buffered.start(0);
if (end > start) {
sb.remove(start, end);
ms.setLiveSeekableRange(end, end + 5);
}
// console.debug("VideoRTC.buffered", start, end);
} catch (e) {
// console.debug(e);
}
});
@@ -406,55 +431,12 @@ class VideoRTC extends HTMLElement {
}
}
internalRTC() {
console.debug("VideoRTC.internalRTC");
onwebrtc() {
const pc = new RTCPeerConnection(this.pcConfig);
let mseCodecs = "";
/** @type {HTMLVideoElement} */
const video2 = document.createElement("video");
video2.addEventListener("loadeddata", () => {
console.debug("VideoRTC.video.loadeddata", video2.readyState, pc.connectionState);
if (pc.connectionState === "connected" || pc.connectionState === "connecting") {
// Video+Audio > Video, H265 > H264, Video > Audio, WebRTC > MSE
let rtcPriority = 0, msePriority = 0;
/** @type {MediaStream} */
const rtc = video2.srcObject;
if (rtc.getVideoTracks().length > 0) rtcPriority += 0x220;
if (rtc.getAudioTracks().length > 0) rtcPriority += 0x102;
if (mseCodecs.indexOf("hvc1.") >= 0) msePriority += 0x230;
if (mseCodecs.indexOf("avc1.") >= 0) msePriority += 0x210;
if (mseCodecs.indexOf("mp4a.") >= 0) msePriority += 0x101;
if (rtcPriority >= msePriority) {
console.debug("VideoRTC.select RTC mode", rtcPriority, msePriority);
this.video.controls = true;
this.video.srcObject = rtc;
this.play();
this.pcState = WebSocket.OPEN;
this.wsState = WebSocket.CLOSED;
this.ws.close();
this.ws = null;
} else {
console.debug("VideoRTC.select MSE mode", rtcPriority, msePriority);
pc.close();
this.pcState = WebSocket.CLOSED;
this.pc = null;
}
}
video2.srcObject = null;
}, {once: true});
video2.addEventListener("loadeddata", ev => this.onpcvideo(ev), {once: true});
pc.addEventListener("icecandidate", ev => {
const candidate = ev.candidate ? ev.candidate.toJSON().candidate : "";
@@ -462,8 +444,6 @@ class VideoRTC extends HTMLElement {
});
pc.addEventListener("track", ev => {
console.debug("VideoRTC.pc.track", ev.streams.length);
// when stream already init
if (video2.srcObject !== null) return;
@@ -477,30 +457,29 @@ class VideoRTC extends HTMLElement {
});
pc.addEventListener("connectionstatechange", () => {
console.debug("VideoRTC.pc.connectionstatechange", this.pc.connectionState);
if (pc.connectionState === "failed" || pc.connectionState === "disconnected") {
pc.close(); // stop next events
this.pcState = WebSocket.CLOSED;
this.pc = null;
if (this.wsState === WebSocket.CLOSED && this.isConnected) {
this.connectedCallback();
}
this.onconnect();
}
});
this.onmessage["webrtc"] = msg => {
switch (msg.type) {
case "webrtc/candidate":
pc.addIceCandidate({candidate: msg.value, sdpMid: ""}).catch(() => console.debug);
pc.addIceCandidate({
candidate: msg.value,
sdpMid: "0"
}).catch(() => console.debug);
break;
case "webrtc/answer":
pc.setRemoteDescription({type: "answer", sdp: msg.value}).catch(() => console.debug);
break;
case "mse":
mseCodecs = msg.value;
pc.setRemoteDescription({
type: "answer",
sdp: msg.value
}).catch(() => console.debug);
break;
case "error":
if (msg.value.indexOf("webrtc/offer") < 0) return;
@@ -522,52 +501,88 @@ class VideoRTC extends HTMLElement {
this.pc = pc;
}
internalMJPEG() {
console.debug("VideoRTC.internalMJPEG");
/**
* @param ev {Event}
*/
onpcvideo(ev) {
if (!this.pc) return;
/** @type {HTMLVideoElement} */
const video2 = ev.target;
const state = this.pc.connectionState;
// Firefox doesn't support pc.connectionState
if (state === "connected" || state === "connecting" || !state) {
// 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;
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.play();
this.pcState = WebSocket.OPEN;
this.wsState = WebSocket.CLOSED;
this.ws.close();
this.ws = null;
} else {
this.pcState = WebSocket.CLOSED;
this.pc.close();
this.pc = null;
}
}
video2.srcObject = null;
}
onmjpeg() {
this.ondata = data => {
this.video.controls = false;
this.video.poster = "data:image/jpeg;base64," + VideoRTC.btoa(data);
};
this.send({type: "mjpeg"});
this.video.controls = false;
}
internalMP4() {
console.debug("VideoRTC.internalMP4");
onmp4() {
/** @type {HTMLCanvasElement} **/
const canvas = document.createElement("canvas");
/** @type {CanvasRenderingContext2D} */
let context;
/** @type {HTMLVideoElement} */
let video2;
const video2 = document.createElement("video");
video2.autoplay = true;
video2.playsInline = true;
video2.muted = true;
this.ondata = data => {
// first video with default position (set container size)
// second video with position=absolute and top=0px
if (video2) {
this.removeChild(this.video);
this.video.src = "";
this.video = video2;
video2.style.position = "";
video2.style.top = "";
video2.addEventListener("loadeddata", ev => {
if (!context) {
canvas.width = video2.videoWidth;
canvas.height = video2.videoHeight;
context = canvas.getContext('2d');
}
video2 = this.video.cloneNode();
video2.style.position = "absolute";
video2.style.top = "0px";
this.appendChild(video2);
context.drawImage(video2, 0, 0, canvas.width, canvas.height);
video2.src = "data:video/mp4;base64," + VideoRTC.btoa(data);
video2.play().catch(() => console.log);
};
this.ws.addEventListener("close", () => {
if (!video2) return;
this.removeChild(video2);
video2.src = "";
this.video.controls = false;
this.video.poster = canvas.toDataURL("image/jpeg");
});
this.ondata = data => {
video2.src = "data:video/mp4;base64," + VideoRTC.btoa(data);
};
this.send({type: "mp4", value: this.codecs("mp4")});
this.video.controls = false;
}
static btoa(buffer) {
@@ -580,5 +595,3 @@ class VideoRTC extends HTMLElement {
return window.btoa(binary);
}
}
customElements.define("video-rtc", VideoRTC);
+98
View File
@@ -0,0 +1,98 @@
import {VideoRTC} from "./video-rtc.js";
class VideoStream extends VideoRTC {
set divMode(value) {
this.querySelector(".mode").innerText = value;
this.querySelector(".status").innerText = "";
}
set divError(value) {
const state = this.querySelector(".mode").innerText;
if (state !== "loading") return;
this.querySelector(".mode").innerText = "error";
this.querySelector(".status").innerText = value;
}
/**
* Custom GUI
*/
oninit() {
console.debug("stream.oninit");
super.oninit();
this.innerHTML = `
<style>
video-stream {
position: relative;
}
.info {
position: absolute;
top: 0;
left: 0;
right: 0;
padding: 12px;
color: white;
display: flex;
justify-content: space-between;
pointer-events: none;
}
</style>
<div class="info">
<div class="status"></div>
<div class="mode"></div>
</div>
`;
const info = this.querySelector(".info")
this.insertBefore(this.video, info);
}
onconnect() {
console.debug("stream.onconnect");
const result = super.onconnect();
if (result) this.divMode = "loading";
return result;
}
ondisconnect() {
console.debug("stream.ondisconnect");
super.ondisconnect();
}
onopen() {
console.debug("stream.onopen");
const result = super.onopen();
this.onmessage["stream"] = msg => {
console.debug("stream.onmessge", msg);
switch (msg.type) {
case "error":
this.divError = msg.value;
break;
case "mse":
case "mp4":
case "mjpeg":
this.divMode = msg.type.toUpperCase();
break;
}
}
return result;
}
onclose() {
console.debug("stream.onclose");
return super.onclose();
}
onpcvideo(ev) {
console.debug("stream.onpcvideo");
super.onpcvideo(ev);
if (this.pcState !== WebSocket.CLOSED) {
this.divMode = "RTC";
}
}
}
customElements.define("video-stream", VideoStream);
+1 -1
View File
@@ -48,7 +48,7 @@
console.debug('ws.onmessage', msg);
if (msg.type === 'webrtc/candidate') {
pc.addIceCandidate({candidate: msg.value, sdpMid: ''});
pc.addIceCandidate({candidate: msg.value, sdpMid: '0'});
} else if (msg.type === 'webrtc/answer') {
pc.setRemoteDescription({type: 'answer', sdp: msg.value});
}