Compare commits

..

55 Commits

Author SHA1 Message Date
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
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
49 changed files with 2837 additions and 475 deletions
+7 -5
View File
@@ -2,9 +2,9 @@ name: release
on:
workflow_dispatch:
push:
tags:
- 'v*'
# push:
# tags:
# - 'v*'
jobs:
build-and-release:
@@ -19,6 +19,8 @@ jobs:
run: |
#!/bin/bash
esport CGO_ENABLED=0
mkdir artifacts
export GOOS=windows
export GOARCH=amd64
@@ -63,12 +65,12 @@ jobs:
export GOOS=darwin
export GOARCH=amd64
export FILENAME=go2rtc_mac_amd64.zip
export FILENAME=artifacts/go2rtc_mac_amd64.zip
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc
export GOOS=darwin
export GOARCH=arm64
export FILENAME=go2rtc_mac_arm64.zip
export FILENAME=artifacts/go2rtc_mac_arm64.zip
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc
parallel --jobs $(nproc) "upx {}" ::: artifacts/go2rtc_linux_*
+4 -5
View File
@@ -33,13 +33,12 @@ 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),
# Install ffmpeg, tini (for signal handling),
# and other common tools for the echo source.
RUN apk add --no-cache tini ffmpeg bash curl jq
@@ -55,8 +54,8 @@ RUN if [ "${TARGETARCH}" = "amd64" ]; then apk add --no-cache libva-intel-driver
COPY --from=rootfs / /
RUN chmod a+x /run.sh && mkdir -p /config
ENTRYPOINT ["/sbin/tini", "--"]
VOLUME /config
WORKDIR /config
CMD ["/run.sh"]
CMD ["go2rtc", "-config", "/config/go2rtc.yaml"]
+139 -53
View File
@@ -7,7 +7,7 @@ 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), [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) or [MJPEG](#module-mjpeg)
- 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 (Safari only, [read more](https://github.com/AlexxIT/Blog/issues/5))
- on the fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg)
@@ -27,16 +27,50 @@ 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)
* [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
**Developers:**
@@ -59,6 +93,10 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2
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)
@@ -68,20 +106,20 @@ 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 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).
[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`. go2rtc will search this file in current work dirrectory by default.
- by default, you need to config only your `streams` links
- by default go2rtc will search `go2rtc.yaml` in the current work dirrectory
- `api` server will start on default **1984 port** (TCP)
- `rtsp` server will start on default **8554 port** (TCP)
- `webrtc` will use port **8555** (TCP/UDP) for connections
- `ffmpeg` will use default transcoding options
Configuration options and a complete list of settings can be found in [the wiki](https://github.com/AlexxIT/go2rtc/wiki/Configuration).
Available modules:
- [streams](#module-streams)
@@ -89,14 +127,13 @@ 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)
- [hass](#module-hass) - Home Assistant integration
- [log](#module-log) - logs config
Full default config [example](https://github.com/AlexxIT/go2rtc/wiki/Configuration).
### Module: Streams
**go2rtc** support different stream source types. You can config one or multiple links of any type as stream source.
@@ -359,6 +396,8 @@ go2rtc has simple HTML page (`stream.html`) with support params in URL:
```yaml
api:
listen: ":1984" # default ":1984", HTTP API port ("" - disabled)
username: "admin" # default "", Basic auth for WebUI
password: "pass" # default "", Basic auth for WebUI
base_path: "/rtc" # default "", API prefix for serve on suburl (/api => /rtc/api)
static_dir: "www" # default "", folder for static files (custom web interface)
origin: "*" # default "", allow CORS requests (only * supported)
@@ -366,7 +405,7 @@ api:
**PS:**
- go2rtc doesn't provide HTTPS or password protection. Use [Nginx](https://nginx.org/) or [Ngrok](#module-ngrok) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on) for this tasks
- go2rtc doesn't provide HTTPS. Use [Nginx](https://nginx.org/) or [Ngrok](#module-ngrok) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on) for this tasks
- you can access microphone (for 2-way audio) only with HTTPS ([read more](https://stackoverflow.com/questions/52759992/how-to-access-camera-and-microphone-in-chrome-without-https))
- MJPEG over WebSocket plays better than native MJPEG because Chrome [bug](https://bugs.chromium.org/p/chromium/issues/detail?id=527446)
- MP4 over WebSocket was created only for Apple iOS because it doesn't support MSE and native MP4
@@ -375,20 +414,25 @@ api:
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.
@@ -399,7 +443,7 @@ WebRTC usually works without problems in the local network. But external access
```yaml
webrtc:
listen: ":8555" # address of your local server and port (TCP/UDP)
listen: ":8555" # address of your local server and port (TCP/UDP)
```
**Static public IP**
@@ -507,24 +551,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. RTSPtoWebRTC > Configure > STUN server: `stun.l.google.com:19302`
3. 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:
@@ -541,13 +590,28 @@ Provides several features:
1. MSE stream (fMP4 over WebSocket)
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, doesn't work in Safari
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
**Important.** For stream as MJPEG format, your source MUST contain the MJPEG codec. If your stream has a MJPEG codec - you can receive **MJPEG stream** or **JPEG snapshots** via API.
@@ -614,21 +678,42 @@ If you need Web interface protection without Home Assistant Add-on - you need to
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/)
@@ -637,8 +722,9 @@ PS. Additionally WebRTC will try to use the 8555 UDP port for transmit encrypted
**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**
@@ -692,17 +778,17 @@ streams:
**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.
+29 -7
View File
@@ -8,6 +8,7 @@ import (
"net/http"
"os"
"strconv"
"strings"
"sync"
)
@@ -15,6 +16,8 @@ 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"`
@@ -52,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() {
@@ -87,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)
})
}
+6 -1
View File
@@ -9,11 +9,16 @@ import (
)
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.NotFound(w, r)
http.Error(w, "", http.StatusNotFound)
return
}
if _, err = w.Write(data); err != nil {
+3 -1
View File
@@ -81,7 +81,9 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
for {
msg := new(Message)
if err = ws.ReadJSON(msg); err != nil {
log.Trace().Err(err).Caller().Send()
if !websocket.IsCloseError(err, websocket.CloseNoStatusReceived) {
log.Trace().Err(err).Caller().Send()
}
_ = ws.Close()
break
}
+8 -5
View File
@@ -8,12 +8,13 @@ import (
"gopkg.in/yaml.v3"
"io"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"time"
)
var Version = "0.1-rc.9"
var Version = "1.1.1"
var UserAgent = "go2rtc/" + Version
var ConfigPath string
@@ -52,8 +53,10 @@ func Init() {
}
if ConfigPath != "" {
if cwd, err := os.Getwd(); err == nil {
ConfigPath = path.Join(cwd, ConfigPath)
if !filepath.IsAbs(ConfigPath) {
if cwd, err := os.Getwd(); err == nil {
ConfigPath = filepath.Join(cwd, ConfigPath)
}
}
Info["config_path"] = ConfigPath
}
@@ -81,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 {
+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}
}
+3
View File
@@ -67,6 +67,7 @@ var defaults = map[string]string{
"pcma/48000": "-c:a pcm_alaw -ar:a 48000 -ac:a 1",
"aac": "-c:a aac", // keep sample rate and channels
"aac/16000": "-c:a aac -ar:a 16000 -ac:a 1",
"mp3": "-c:a libmp3lame -q:a 8",
// hardware Intel and AMD on Linux
// better not to set `-async_depth:v 1` like for QSV, because framedrops
@@ -141,6 +142,8 @@ func parseArgs(s string) *Args {
s += "?video"
case args.audio > 0 && args.video == 0:
s += "?audio"
default:
s += "?video&audio"
}
args.input = strings.Replace(defaults["rtsp"], "{input}", s, 1)
} else if strings.HasPrefix(s, "device?") {
+261
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()
}
}
+27 -23
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
}
@@ -83,7 +105,9 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
cons := &mp4.Consumer{
RemoteAddr: r.RemoteAddr,
UserAgent: r.UserAgent(),
Medias: streamer.ParseQuery(r.URL.Query()),
}
cons.Listen(func(msg interface{}) {
if data, ok := msg.([]byte); ok {
if _, err := w.Write(data); err != nil && exit != nil {
@@ -135,23 +159,3 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
duration.Stop()
}
}
func isChromeFirst(w http.ResponseWriter, r *http.Request) bool {
// Chrome 105 does two requests: without Range and with `Range: bytes=0-`
if strings.Contains(r.UserAgent(), " Chrome/") {
if r.Header.Values("Range") == nil {
w.Header().Set("Content-Type", "video/mp4")
w.WriteHeader(http.StatusOK)
return true
}
}
return false
}
func isSafari(w http.ResponseWriter, r *http.Request) bool {
if r.Header.Get("Range") == "bytes=0-1" {
handlerKeyframe(w, r)
return true
}
return false
}
+11 -6
View File
@@ -22,8 +22,6 @@ func handlerWSMSE(tr *api.Transport, msg *api.Message) error {
RemoteAddr: tr.Request.RemoteAddr,
UserAgent: tr.Request.UserAgent(),
}
cons.UserAgent = tr.Request.UserAgent()
cons.RemoteAddr = tr.Request.RemoteAddr
if codecs, ok := msg.Value.(string); ok {
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MSE consumer")
@@ -71,7 +69,11 @@ func handlerWSMP4(tr *api.Transport, msg *api.Message) error {
return errors.New(api.StreamNotFound)
}
cons := &mp4.Segment{OnlyKeyframe: true}
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")
@@ -104,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)
}
}
+20 -46
View File
@@ -3,25 +3,29 @@ package rtsp
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog"
"net"
"net/url"
"strings"
)
func Init() {
var conf struct {
Mod struct {
Listen string `yaml:"listen" json:"listen"`
Username string `yaml:"username" json:"-"`
Password string `yaml:"password" json:"-"`
Listen string `yaml:"listen" json:"listen"`
Username string `yaml:"username" json:"-"`
Password string `yaml:"password" json:"-"`
DefaultQuery string `yaml:"default_query" json:"default_query"`
} `yaml:"rtsp"`
}
// default config
conf.Mod.Listen = ":8554"
conf.Mod.DefaultQuery = "video&audio"
app.LoadConfig(&conf)
app.Info["rtsp"] = conf.Mod
@@ -49,6 +53,10 @@ func Init() {
log.Info().Str("addr", address).Msg("[rtsp] listen")
if query, err := url.ParseQuery(conf.Mod.DefaultQuery); err == nil {
defaultMedias = mp4.ParseQuery(query)
}
go func() {
for {
conn, err := ln.Accept()
@@ -78,6 +86,7 @@ var Port string
var log zerolog.Logger
var handlers []Handler
var defaultMedias []*streamer.Media
func rtspHandler(url string) (streamer.Producer, error) {
backchannel := true
@@ -162,7 +171,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]")
@@ -226,45 +242,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},
}
}
}
+3 -1
View File
@@ -91,7 +91,9 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
consumer.tracks = append(consumer.tracks, consTrack)
producers = append(producers, prod)
break producers
if !consMedia.MatchAll() {
break producers
}
}
}
}
+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
}
+24 -1
View File
@@ -1,6 +1,7 @@
package h264
import (
"bytes"
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
@@ -27,7 +28,8 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
}
// Fix TP-Link Tapo TC70: sends SPS and PPS with packet.Marker = true
if packet.Marker {
// Reolink Duo 2: sends SPS with Marker and PPS without
if packet.Marker && len(payload) < 128 {
switch NALUType(payload) {
case NALUTypeSPS, NALUTypePPS:
buf = append(buf, payload...)
@@ -68,6 +70,27 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
if len(buf) > 0 {
payload = append(buf, payload...)
buf = buf[:0]
} else {
// some Chinese buggy cameras has single packet with SPS+PPS+IFrame separated by 00 00 00 01
// https://github.com/AlexxIT/WebRTC/issues/391
// https://github.com/AlexxIT/WebRTC/issues/392
for i := 0; i < len(payload); {
if i+4 >= len(payload) {
break
}
size := bytes.Index(payload[i+4:], []byte{0, 0, 0, 1})
if size < 0 {
if i == 0 {
break
}
size = len(payload) - (i + 4)
}
binary.BigEndian.PutUint32(payload[i:], uint32(size))
i += size + 4
}
}
//log.Printf("[AVC] %v, len: %d, ts: %10d, seq: %d", Types(payload), len(payload), packet.Timestamp, packet.SequenceNumber)
+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)
}
+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
+30 -1
View File
@@ -24,6 +24,16 @@ type Consumer struct {
send uint32
}
// ParseQuery - like usual parse, but with mp4 param handler
func ParseQuery(query map[string][]string) []*streamer.Media {
if query["mp4"] != nil {
cons := Consumer{}
return cons.GetMedias()
}
return streamer.ParseQuery(query)
}
const (
waitNone byte = iota
waitKeyframe
@@ -140,14 +150,33 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
push = wrapper(push)
}
return track.Bind(push)
case streamer.CodecOpus, streamer.CodecMP3, streamer.CodecPCMU, streamer.CodecPCMA:
push := func(packet *rtp.Packet) error {
if c.wait != waitNone {
return nil
}
buf := c.muxer.Marshal(trackID, packet)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
return nil
}
return track.Bind(push)
}
panic("unsupported codec")
}
func (c *Consumer) MimeCodecs() string {
return c.muxer.MimeCodecs(c.codecs)
}
func (c *Consumer) MimeType() string {
return c.muxer.MimeType(c.codecs)
return `video/mp4; codecs="` + c.MimeCodecs() + `"`
}
func (c *Consumer) Init() ([]byte, error) {
+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)
}
@@ -1,6 +1,8 @@
package mp4f
package mp4
import (
"encoding/hex"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/av"
@@ -14,6 +16,7 @@ import (
type Consumer struct {
streamer.Element
Medias []*streamer.Media
UserAgent string
RemoteAddr string
@@ -26,6 +29,10 @@ type Consumer struct {
}
func (c *Consumer) GetMedias() []*streamer.Media {
if c.Medias != nil {
return c.Medias
}
return []*streamer.Media{
{
Kind: streamer.KindVideo,
@@ -96,7 +103,17 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
return track.Bind(push)
case streamer.CodecAAC:
stream, _ := aacparser.NewCodecDataFromMPEG4AudioConfigBytes([]byte{20, 8})
s := streamer.Between(codec.FmtpLine, "config=", ";")
b, err := hex.DecodeString(s)
if err != nil {
return nil
}
stream, err := aacparser.NewCodecDataFromMPEG4AudioConfigBytes(b)
if err != nil {
return nil
}
c.mimeType += ",mp4a.40.2"
c.streams = append(c.streams, stream)
@@ -126,6 +143,11 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
return nil
}
if codec.IsRTP() {
wrapper := aac.RTPDepay(track)
push = wrapper(push)
}
return track.Bind(push)
}
+174
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)
}
+21 -53
View File
@@ -2,7 +2,6 @@ package rtsp
import (
"bufio"
"bytes"
"crypto/tls"
"encoding/binary"
"errors"
@@ -78,6 +77,7 @@ type Conn struct {
// public
Backchannel bool
SessionName string
Medias []*streamer.Media
Session string
@@ -280,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
}
@@ -310,7 +310,7 @@ func (c *Conn) Describe() error {
}
if val := res.Header.Get("Content-Base"); val != "" {
c.URL, err = url.Parse(val)
c.URL, err = urlParse(val)
if err != nil {
return err
}
@@ -347,7 +347,7 @@ func (c *Conn) Describe() error {
func (c *Conn) Setup() error {
for _, media := range c.Medias {
_, err := c.SetupMedia(media, media.Codecs[0])
_, err := c.SetupMedia(media, media.Codecs[0], true)
if err != nil {
return err
}
@@ -356,11 +356,12 @@ func (c *Conn) Setup() error {
return nil
}
func (c *Conn) SetupMedia(
media *streamer.Media, codec *streamer.Codec,
) (*streamer.Track, error) {
c.stateMu.Lock()
defer c.stateMu.Unlock()
func (c *Conn) SetupMedia(media *streamer.Media, codec *streamer.Codec, first bool) (*streamer.Track, error) {
// TODO: rewrite recoonection and first flag
if first {
c.stateMu.Lock()
defer c.stateMu.Unlock()
}
if c.state != StateConn && c.state != StateSetup {
return nil, fmt.Errorf("RTSP SETUP from wrong state: %s", c.state)
@@ -379,7 +380,7 @@ func (c *Conn) SetupMedia(
}
rawURL += media.Control
}
trackURL, err := url.Parse(rawURL)
trackURL, err := urlParse(rawURL)
if err != nil {
return nil, err
}
@@ -412,7 +413,7 @@ func (c *Conn) SetupMedia(
for _, newMedia := range c.Medias {
if newMedia.Control == media.Control {
return c.SetupMedia(newMedia, newMedia.Codecs[0])
return c.SetupMedia(newMedia, newMedia.Codecs[0], false)
}
}
}
@@ -618,7 +619,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
}
@@ -654,6 +655,12 @@ func (c *Conn) Accept() error {
}
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)
}
@@ -792,12 +799,12 @@ func (c *Conn) Handle() (err error) {
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)
@@ -875,42 +882,3 @@ func (c *Conn) bindTrack(
return track.Bind(push)
}
type RTCP struct {
Channel byte
Header rtcp.Header
Packets []rtcp.Packet
}
const sdpHeader = `v=0
o=- 0 0 IN IP4 0.0.0.0
s=-
t=0 0`
func UnmarshalSDP(rawSDP []byte) ([]*streamer.Media, error) {
medias, err := streamer.UnmarshalSDP(rawSDP)
if err != nil {
// fix SDP header for some cameras
i := bytes.Index(rawSDP, []byte("\nm="))
if i > 0 {
rawSDP = append([]byte(sdpHeader), rawSDP[i:]...)
medias, err = streamer.UnmarshalSDP(rawSDP)
}
if err != nil {
return nil, err
}
}
// fix bug in ONVIF spec
// https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec-v241.pdf
for _, media := range medias {
switch media.Direction {
case streamer.DirectionRecvonly, "":
media.Direction = streamer.DirectionSendonly
case streamer.DirectionSendonly:
media.Direction = streamer.DirectionRecvonly
}
}
return medias, nil
}
+68
View File
@@ -0,0 +1,68 @@
package rtsp
import (
"bytes"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtcp"
"net/url"
"strings"
)
type RTCP struct {
Channel byte
Header rtcp.Header
Packets []rtcp.Packet
}
const sdpHeader = `v=0
o=- 0 0 IN IP4 0.0.0.0
s=-
t=0 0`
func UnmarshalSDP(rawSDP []byte) ([]*streamer.Media, error) {
medias, err := streamer.UnmarshalSDP(rawSDP)
if err != nil {
// fix SDP header for some cameras
i := bytes.Index(rawSDP, []byte("\nm="))
if i > 0 {
rawSDP = append([]byte(sdpHeader), rawSDP[i:]...)
medias, err = streamer.UnmarshalSDP(rawSDP)
}
if err != nil {
return nil, err
}
}
// fix bug in ONVIF spec
// https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec-v241.pdf
for _, media := range medias {
switch media.Direction {
case streamer.DirectionRecvonly, "":
media.Direction = streamer.DirectionSendonly
case streamer.DirectionSendonly:
media.Direction = streamer.DirectionRecvonly
}
}
return medias, nil
}
// urlParse fix bugs:
// 1. Content-Base: rtsp://::ffff:192.168.1.123/onvif/profile.1/
// 2. Content-Base: rtsp://rtsp://turret2-cam.lan:554/stream1/
func urlParse(rawURL string) (*url.URL, error) {
if strings.HasPrefix(rawURL, "rtsp://rtsp://") {
rawURL = rawURL[7:]
}
u, err := url.Parse(rawURL)
if err != nil && strings.HasSuffix(err.Error(), "after host") {
if i1 := strings.Index(rawURL, "://"); i1 > 0 {
if i2 := strings.IndexByte(rawURL[i1+3:], '/'); i2 > 0 {
return urlParse(rawURL[:i1+3+i2] + ":" + rawURL[i1+3+i2:])
}
}
}
return u, err
}
+20
View File
@@ -0,0 +1,20 @@
package rtsp
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestURLParse(t *testing.T) {
// https://github.com/AlexxIT/WebRTC/issues/395
base := "rtsp://::ffff:192.168.1.123/onvif/profile.1/"
u, err := urlParse(base)
assert.Empty(t, err)
assert.Equal(t, "::ffff:192.168.1.123:", u.Host)
// https://github.com/AlexxIT/go2rtc/issues/208
base = "rtsp://rtsp://turret2-cam.lan:554/stream1/"
u, err = urlParse(base)
assert.Empty(t, err)
assert.Equal(t, "turret2-cam.lan:554", u.Host)
}
+35 -7
View File
@@ -9,7 +9,26 @@ import (
// Element Producer
func (c *Conn) GetMedias() []*streamer.Media {
return c.Medias
if c.Medias != nil {
return c.Medias
}
return []*streamer.Media{
{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecAll},
},
},
{
Kind: streamer.KindAudio,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecAll},
},
},
}
}
func (c *Conn) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
@@ -26,7 +45,7 @@ func (c *Conn) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.
return streamer.NewTrack(codec, media.Direction)
}
track, err := c.SetupMedia(media, codec)
track, err := c.SetupMedia(media, codec, true)
if err != nil {
return nil
}
@@ -63,11 +82,20 @@ func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.
codec := track.Codec.Clone()
codec.PayloadType = uint8(96 + i)
for i, m := range c.Medias {
if m == media {
media.Codecs = []*streamer.Codec{codec}
c.Medias[i] = media
break
if media.MatchAll() {
// fill consumer medias list
c.Medias = append(c.Medias, &streamer.Media{
Kind: media.Kind, Direction: media.Direction,
Codecs: []*streamer.Codec{codec},
})
} else {
// find consumer media and replace codec with right one
for i, m := range c.Medias {
if m == media {
media.Codecs = []*streamer.Codec{codec}
c.Medias[i] = media
break
}
}
}
+65 -10
View File
@@ -33,9 +33,12 @@ const (
CodecAAC = "MPEG4-GENERIC"
CodecOpus = "OPUS" // payloadType: 111
CodecG722 = "G722"
CodecMPA = "MPA" // payload: 14
CodecMP3 = "MPA" // payload: 14, aka MPEG-1 Layer III
CodecELD = "ELD" // AAC-ELD
CodecAll = "ALL"
CodecAny = "ANY"
)
const PayloadTypeRAW byte = 255
@@ -44,7 +47,7 @@ func GetKind(name string) string {
switch name {
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG:
return KindVideo
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMPA, CodecELD:
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecELD:
return KindAudio
}
return ""
@@ -60,7 +63,6 @@ type Media struct {
MID string `json:"mid,omitempty"` // TODO: fixme?
Control string `json:"control,omitempty"` // TODO: fixme?
Title string `json:"title,omitempty"` // TODO: fixme?
}
func (m *Media) String() string {
@@ -112,10 +114,6 @@ func (m *Media) MatchMedia(media *Media) *Codec {
}
for _, localCodec := range m.Codecs {
if media.Codecs == nil {
return localCodec
}
for _, remoteCodec := range media.Codecs {
if localCodec.Match(remoteCodec) {
return localCodec
@@ -125,6 +123,10 @@ func (m *Media) MatchMedia(media *Media) *Codec {
return nil
}
func (m *Media) MatchAll() bool {
return len(m.Codecs) > 0 && m.Codecs[0].Name == CodecAll
}
// Codec take best from:
// - deepch/vdk/av.CodecData
// - pion/webrtc.RTPCodecCapability
@@ -154,6 +156,11 @@ func (c *Codec) Clone() *Codec {
}
func (c *Codec) Match(codec *Codec) bool {
switch codec.Name {
case CodecAll, CodecAny:
return true
}
return c.Name == codec.Name &&
(c.ClockRate == codec.ClockRate || codec.ClockRate == 0) &&
(c.Channels == codec.Channels || codec.Channels == 0)
@@ -183,8 +190,22 @@ func UnmarshalSDP(rawSDP []byte) ([]*Media, error) {
return medias, nil
}
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)
@@ -272,7 +293,7 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
c.Name = CodecPCMA
c.ClockRate = 8000
case "14":
c.Name = CodecMPA
c.Name = CodecMP3
c.ClockRate = 44100
case "26":
c.Name = CodecJPEG
@@ -285,6 +306,40 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
return c
}
func ParseQuery(query map[string][]string) (medias []*Media) {
// set media candidates from query list
for key, values := range query {
switch key {
case KindVideo, KindAudio:
for _, value := range values {
media := &Media{Kind: key, Direction: DirectionRecvonly}
for _, name := range strings.Split(value, ",") {
name = strings.ToUpper(name)
// check aliases
switch name {
case "", "COPY":
name = CodecAny
case "MJPEG":
name = CodecJPEG
case "AAC":
name = CodecAAC
case "MP3":
name = CodecMP3
}
media.Codecs = append(media.Codecs, &Codec{Name: name})
}
medias = append(medias, media)
}
}
}
return
}
func atoi(s string) (i int) {
i, _ = strconv.Atoi(s)
return
+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)
}
}
+200
View File
@@ -0,0 +1,200 @@
package ts
import (
"bytes"
"encoding/hex"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/aacparser"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/ts"
"github.com/pion/rtp"
"sync/atomic"
"time"
)
type Consumer struct {
streamer.Element
UserAgent string
RemoteAddr string
buf *bytes.Buffer
muxer *ts.Muxer
mimeType string
streams []av.CodecData
start bool
init []byte
send uint32
}
func (c *Consumer) GetMedias() []*streamer.Media {
return []*streamer.Media{
{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecH264},
},
},
//{
// Kind: streamer.KindAudio,
// Direction: streamer.DirectionRecvonly,
// Codecs: []*streamer.Codec{
// {Name: streamer.CodecAAC},
// },
//},
}
}
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
codec := track.Codec
trackID := int8(len(c.streams))
switch codec.Name {
case streamer.CodecH264:
sps, pps := h264.GetParameterSet(codec.FmtpLine)
stream, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
if err != nil {
return nil
}
if len(c.mimeType) > 0 {
c.mimeType += ","
}
// TODO: fixme
// some devices won't play high level
if stream.RecordInfo.AVCLevelIndication <= 0x29 {
c.mimeType += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
} else {
c.mimeType += "avc1.640029"
}
c.streams = append(c.streams, stream)
pkt := av.Packet{Idx: trackID, CompositionTime: time.Millisecond}
ts2time := time.Second / time.Duration(codec.ClockRate)
push := func(packet *rtp.Packet) error {
if packet.Version != h264.RTPPacketVersionAVC {
return nil
}
if !c.start {
return nil
}
pkt.Data = packet.Payload
newTime := time.Duration(packet.Timestamp) * ts2time
if pkt.Time > 0 {
pkt.Duration = newTime - pkt.Time
}
pkt.Time = newTime
if err = c.muxer.WritePacket(pkt); err != nil {
return err
}
// clone bytes from buffer, so next packet won't overwrite it
buf := append([]byte{}, c.buf.Bytes()...)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
c.buf.Reset()
return nil
}
if codec.IsRTP() {
wrapper := h264.RTPDepay(track)
push = wrapper(push)
}
return track.Bind(push)
case streamer.CodecAAC:
s := streamer.Between(codec.FmtpLine, "config=", ";")
b, err := hex.DecodeString(s)
if err != nil {
return nil
}
stream, err := aacparser.NewCodecDataFromMPEG4AudioConfigBytes(b)
if err != nil {
return nil
}
if len(c.mimeType) > 0 {
c.mimeType += ","
}
c.mimeType += "mp4a.40.2"
c.streams = append(c.streams, stream)
pkt := av.Packet{Idx: trackID, CompositionTime: time.Millisecond}
ts2time := time.Second / time.Duration(codec.ClockRate)
push := func(packet *rtp.Packet) error {
if !c.start {
return nil
}
pkt.Data = packet.Payload
newTime := time.Duration(packet.Timestamp) * ts2time
if pkt.Time > 0 {
pkt.Duration = newTime - pkt.Time
}
pkt.Time = newTime
if err := c.muxer.WritePacket(pkt); err != nil {
return err
}
// clone bytes from buffer, so next packet won't overwrite it
buf := append([]byte{}, c.buf.Bytes()...)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
c.buf.Reset()
return nil
}
if codec.IsRTP() {
wrapper := aac.RTPDepay(track)
push = wrapper(push)
}
return track.Bind(push)
}
panic("unsupported codec")
}
func (c *Consumer) MimeCodecs() string {
return c.mimeType
}
func (c *Consumer) Init() ([]byte, error) {
c.buf = bytes.NewBuffer(nil)
c.muxer = ts.NewMuxer(c.buf)
// first packet will be with header, it's ok
if err := c.muxer.WriteHeader(c.streams); err != nil {
return nil, err
}
data := append([]byte{}, c.buf.Bytes()...)
return data, nil
}
func (c *Consumer) Start() {
c.start = true
}
+2
View File
@@ -56,3 +56,5 @@ pc.ontrack = ev => {
- https://web.dev/i18n/en/fast-playback-with-preload/#manual_buffering
- https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API
- https://chromium.googlesource.com/external/w3c/web-platform-tests/+/refs/heads/master/media-source/mediasource-is-type-supported.html
- https://googlechrome.github.io/samples/media/sourcebuffer-changetype.html
- https://chromestatus.com/feature/5100845653819392
+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 =
+13 -3
View File
@@ -41,7 +41,7 @@
method: 'POST', body: editor.getValue()
}).then(r => {
if (r.ok) {
alert("OK");
alert('OK');
fetch('api/exit', {method: 'POST'});
} else {
r.text().then(alert);
@@ -50,8 +50,18 @@
});
window.addEventListener('load', () => {
fetch('api/config').then(r => r.text()).then(data => {
editor.setValue(data);
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>
+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 =
+9 -17
View File
@@ -89,9 +89,7 @@
const templates = [
'<a href="stream.html?src={name}">stream</a>',
'<a href="webrtc.html?src={name}">2-way-aud</a>',
'<a href="api/stream.mp4?src={name}">mp4</a>',
'<a href="api/stream.mjpeg?src={name}">mjpeg</a>',
'<a href="api/streams?src={name}">info</a>',
'<a href="links.html?src={name}">links</a>',
'<a href="#" data-name="{name}">delete</a>',
];
@@ -133,40 +131,34 @@
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.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).then(r => r.json()).then(data => {
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {
const info = document.querySelector(".info");
info.innerText = `Version: ${data.version}, Config: ${data.config_path}`;
try {
const host = data.host.match(/^[^:]+/)[0];
const port = data.rtsp.listen.match(/[0-9]+$/)[0];
templates.splice(4, 0, `<a href="rtsp://${host}:${port}/{name}">rtsp</a>`);
} catch (e) {
templates.splice(4, 0, `<a href="rtsp://${location.host}:8554/{name}">rtsp</a>`);
}
reload();
});
reload();
</script>
</body>
</html>
+89
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>
+3 -4
View File
@@ -26,8 +26,7 @@ export class VideoRTC extends HTMLElement {
"hvc1.1.6.L153.B0", // H.265 main 5.1 (Chromecast Ultra)
"mp4a.40.2", // AAC LC
"mp4a.40.5", // AAC HE
"mp4a.69", // MP3
"mp4a.6B", // MP3
"opus", // OPUS Chrome
];
/**
@@ -395,11 +394,11 @@ export class VideoRTC extends HTMLElement {
bufLen = 0;
sb.appendBuffer(data);
} else if (sb.buffered && sb.buffered.length) {
const end = sb.buffered.end(sb.buffered.length - 1) - 5;
const end = sb.buffered.end(sb.buffered.length - 1) - 15;
const start = sb.buffered.start(0);
if (end > start) {
sb.remove(start, end);
ms.setLiveSeekableRange(end, end + 5);
ms.setLiveSeekableRange(end, end + 15);
}
// console.debug("VideoRTC.buffered", start, end);
}