Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9268acf1ca | |||
| 55fdf1a647 | |||
| 5fe07aeea0 | |||
| e8b22bca99 | |||
| 5926c1deb9 | |||
| dd98edc48e | |||
| fb1cc7dfc2 | |||
| 7626a09c1c | |||
| db85533e74 | |||
| 5939c8acba | |||
| e985ad23a2 | |||
| 7452eb5e05 | |||
| 5f9788209d | |||
| c07ddb8309 | |||
| 79f1dcfea3 | |||
| 3feaf852af | |||
| 76ec70d2a0 | |||
| 6cef5faf27 | |||
| edb4e6eaad | |||
| 116319f876 | |||
| a0e6005598 | |||
| fd580b6f2c | |||
| 1837e7c86c | |||
| 235f2fde0d | |||
| 35087e0812 | |||
| da08d8e973 | |||
| 757091e43d | |||
| a5c4854aeb | |||
| 4b4deaaaf2 | |||
| 553f5ff0d8 | |||
| 25dc3664fd | |||
| 8dd9991268 | |||
| d633d331bb | |||
| 7d3fbf2ee0 | |||
| c44aaebd65 | |||
| d6259fc0e9 | |||
| 5c657d557a | |||
| 93be5cd92f | |||
| cf6a35d0c7 | |||
| af79e6054b | |||
| 9f3d5e7460 | |||
| abbf180b1b | |||
| 696588e52e | |||
| 3e97ce8b2a | |||
| 722b2827a1 | |||
| 69598b508c | |||
| f49fcc4f68 | |||
| 59347a409e | |||
| 45b25d29b7 | |||
| 49e861d1b0 | |||
| b1701e856a | |||
| a6260d0f56 | |||
| 693d41be87 | |||
| 222dc6a5c2 | |||
| 8fde2b6fe5 | |||
| 15e205cc01 | |||
| 1db9ed4946 | |||
| fd83d151d2 | |||
| 71051e7dcf | |||
| cdb3ee45cf | |||
| ae99c1da03 | |||
| 863cc0c1d7 | |||
| 40494ab87c | |||
| bffe5f0aa2 | |||
| 8241af8b9d | |||
| 5c164de393 | |||
| 8bf5c85b79 | |||
| a42c3e21c9 | |||
| 7016289f14 | |||
| 54302d3bda | |||
| af6b8a400d | |||
| a1b5eae653 | |||
| 1912a43679 | |||
| eca311717a | |||
| d3b2b8fdae | |||
| 3b9a0059df | |||
| a36359f3dd |
@@ -52,7 +52,7 @@ jobs:
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
platforms: |
|
||||
@@ -63,9 +63,10 @@ jobs:
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
- name: Build and push Hardware
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: hardware.Dockerfile
|
||||
@@ -73,3 +74,5 @@ jobs:
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta-hw.outputs.tags }}
|
||||
labels: ${{ steps.meta-hw.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -34,4 +34,4 @@ jobs:
|
||||
path: './website'
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v1
|
||||
uses: actions/deploy-pages@v2
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
name: Test Build and Run
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
pull_request:
|
||||
merge_group:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-test:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest, macos-latest]
|
||||
arch: [amd64, arm64]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: true
|
||||
env:
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '1.20'
|
||||
|
||||
- name: Build Go binary
|
||||
run: go build -ldflags "-s -w" -trimpath -o ./go2rtc
|
||||
|
||||
- name: Test Go binary on linux
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
if [ "${{ matrix.arch }}" = "amd64" ]; then
|
||||
./go2rtc -version
|
||||
else
|
||||
sudo apt-get update && sudo apt-get install -y qemu-user-static
|
||||
sudo cp /usr/bin/qemu-aarch64-static .
|
||||
sudo chown $USER:$USER ./qemu-aarch64-static
|
||||
qemu-aarch64-static ./go2rtc -version
|
||||
fi
|
||||
- name: Test Go binary on macos
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
if [ "${{ matrix.arch }}" = "amd64" ]; then
|
||||
./go2rtc -version
|
||||
else
|
||||
echo "ARM64 architecture is not yet supported on macOS"
|
||||
fi
|
||||
- name: Test Go binary on windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
if ("${{ matrix.arch }}" -eq "amd64") {
|
||||
.\go2rtc* -version
|
||||
} else {
|
||||
Write-Host "ARM64 architecture is not yet supported on Windows"
|
||||
}
|
||||
docker-test:
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
- amd64
|
||||
- "386"
|
||||
- arm/v7
|
||||
- arm64/v8
|
||||
continue-on-error: true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/${{ matrix.platform }}
|
||||
push: false
|
||||
load: true
|
||||
tags: go2rtc-${{ matrix.platform }}
|
||||
- name: test run
|
||||
run: |
|
||||
docker run --platform=linux/${{ matrix.platform }} --rm go2rtc-${{ matrix.platform }} go2rtc -version
|
||||
|
||||
- name: Build and push Hardware
|
||||
if: matrix.platform == 'amd64'
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: hardware.Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: false
|
||||
load: true
|
||||
tags: go2rtc-${{ matrix.platform }}-hardware
|
||||
- name: test run
|
||||
if: matrix.platform == 'amd64'
|
||||
run: |
|
||||
docker run --platform=linux/${{ matrix.platform }} --rm go2rtc-${{ matrix.platform }}-hardware go2rtc -version
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
# 0. Prepare images
|
||||
ARG PYTHON_VERSION="3.11"
|
||||
ARG GO_VERSION="1.19"
|
||||
ARG GO_VERSION="1.20"
|
||||
ARG NGROK_VERSION="3"
|
||||
|
||||
FROM python:${PYTHON_VERSION}-alpine AS base
|
||||
|
||||
@@ -51,11 +51,16 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
||||
* [Source: Tapo](#source-tapo)
|
||||
* [Source: Ivideon](#source-ivideon)
|
||||
* [Source: Hass](#source-hass)
|
||||
* [Source: ISAPI](#source-isapi)
|
||||
* [Source: Roborock](#source-roborock)
|
||||
* [Source: WebRTC](#source-webrtc)
|
||||
* [Source: WebTorrent](#source-webtorrent)
|
||||
* [Incoming sources](#incoming-sources)
|
||||
* [Stream to camera](#stream-to-camera)
|
||||
* [Module: API](#module-api)
|
||||
* [Module: RTSP](#module-rtsp)
|
||||
* [Module: WebRTC](#module-webrtc)
|
||||
* [Module: WebTorrent](#module-webtorrent)
|
||||
* [Module: Ngrok](#module-ngrok)
|
||||
* [Module: Hass](#module-hass)
|
||||
* [Module: MP4](#module-mp4)
|
||||
@@ -161,6 +166,10 @@ Available source types:
|
||||
- [tapo](#source-tapo) - TP-Link Tapo cameras with [two way audio](#two-way-audio) support
|
||||
- [ivideon](#source-ivideon) - public cameras from [Ivideon](https://tv.ivideon.com/) service
|
||||
- [hass](#source-hass) - Home Assistant integration
|
||||
- [isapi](#source-isapi) - two way audio for Hikvision (ISAPI) cameras
|
||||
- [roborock](#source-roborock) - Roborock vacuums with cameras
|
||||
- [webrtc](#source-webrtc) - WebRTC/WHEP sources
|
||||
- [webtorrent](#source-webtorrent) - WebTorrent source from another go2rtc
|
||||
|
||||
Read more about [incoming sources](#incoming-sources)
|
||||
|
||||
@@ -168,8 +177,11 @@ Read more about [incoming sources](#incoming-sources)
|
||||
|
||||
Supported for sources:
|
||||
|
||||
- RTSP cameras with [ONVIF Profile T](https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec.pdf) (back channel connection)
|
||||
- TP-Link Tapo cameras
|
||||
- [RTSP cameras](#source-rtsp) with [ONVIF Profile T](https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec.pdf) (back channel connection)
|
||||
- [TP-Link Tapo](#source-tapo) cameras
|
||||
- [Hikvision ISAPI](#source-isapi) cameras
|
||||
- [Roborock vacuums](#source-roborock) models with cameras
|
||||
- [Any Browser](#incoming-browser) as IP-camera
|
||||
|
||||
Two way audio can be used in browser with [WebRTC](#module-webrtc) technology. The browser will give access to the microphone only for HTTPS sites ([read more](https://stackoverflow.com/questions/52759992/how-to-access-camera-and-microphone-in-chrome-without-https)).
|
||||
|
||||
@@ -414,11 +426,55 @@ streams:
|
||||
|
||||
More cameras, like [Tuya](https://www.home-assistant.io/integrations/tuya/), [ONVIF](https://www.home-assistant.io/integrations/onvif/), and possibly others can also be imported by using [this method](https://github.com/felipecrs/hass-expose-camera-stream-source#importing-home-assistant-cameras-to-go2rtc-andor-frigate).
|
||||
|
||||
### Incoming sources
|
||||
#### Source: ISAPI
|
||||
|
||||
This source type support only backchannel audio for Hikvision ISAPI protocol. So it should be used as second source in addition to the RTSP protocol.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
hikvision1:
|
||||
- rtsp://admin:password@192.168.1.123:554/Streaming/Channels/101
|
||||
- isapi://admin:password@192.168.1.123:80/
|
||||
```
|
||||
|
||||
#### Source: Roborock
|
||||
|
||||
This source type support Roborock vacuums with cameras. Known working models:
|
||||
|
||||
- Roborock S6 MaxV - only video (the vacuum has no microphone)
|
||||
- Roborock S7 MaxV - video and two way audio
|
||||
|
||||
Source support load Roborock credentials from Home Assistant [custom integration](https://github.com/humbertogontijo/homeassistant-roborock). Otherwise, you need to log in to your Roborock account (MiHome account is not supported). Go to: go2rtc WebUI > Add webpage. Copy `roborock://...` source for your vacuum and paste it to `go2rtc.yaml` config.
|
||||
|
||||
If you have graphic pin for your vacuum - add it as numeric pin (lines: 123, 456, 678) to the end of the roborock-link.
|
||||
|
||||
#### Source: WebRTC
|
||||
|
||||
This source type support two connection formats:
|
||||
|
||||
- [WebRTC/WHEP](https://www.ietf.org/id/draft-murillo-whep-01.html) - is an unapproved standard for WebRTC video/audio viewers. But it may already be supported in some third-party software. It is supported in go2rtc.
|
||||
- `go2rtc/WebSocket` - This format is only supported in go2rtc. Unlike WHEP it supports asynchronous WebRTC connection and two way audio.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
webrtc1: webrtc:http://192.168.1.123:1984/api/webrtc?src=dahua1
|
||||
webrtc2: webrtc:ws://192.168.1.123:1984/api/ws?src=dahua1
|
||||
```
|
||||
|
||||
#### Source: WebTorrent
|
||||
|
||||
This source can get a stream from another go2rtc via [WebTorrent](#module-webtorrent) protocol.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
webtorrent1: webtorrent:?share=huofssuxaty00izc&pwd=k3l2j9djeg8v8r7e
|
||||
```
|
||||
|
||||
#### Incoming sources
|
||||
|
||||
By default, go2rtc establishes a connection to the source when any client requests it. Go2rtc drops the connection to the source when it has no clients left.
|
||||
|
||||
- Go2rtc also can accepts incoming sources in [RTSP](#source-rtsp) and [HTTP](#source-http) formats
|
||||
- Go2rtc also can accepts incoming sources in [RTSP](#source-rtsp), [HTTP](#source-http) and **WebRTC/WHIP** formats
|
||||
- Go2rtc won't stop such a source if it has no clients
|
||||
- You can push data only to existing stream (create stream with empty source in config)
|
||||
- You can push multiple incoming sources to same stream
|
||||
@@ -443,9 +499,25 @@ By default, go2rtc establishes a connection to the source when any client reques
|
||||
ffmpeg -re -i BigBuckBunny.mp4 -c copy -f mpegts http://localhost:1984/api/stream.ts?dst=camera1
|
||||
```
|
||||
|
||||
#### Incoming: Browser
|
||||
|
||||
You can turn the browser of any PC or mobile into an IP-camera with support video and two way audio. Or even broadcast your PC screen:
|
||||
|
||||
1. Create empty stream in the `go2rtc.yaml`
|
||||
2. Go to go2rtc WebUI
|
||||
3. Open `links` page for you stream
|
||||
4. Select `camera+microphone` or `display+speaker` option
|
||||
5. Open `webrtc` local page (your go2rtc **should work over HTTPS!**) or `share link` via [WebTorrent](#module-webtorrent) technology (work over HTTPS by default)
|
||||
|
||||
#### Incoming: WebRTC/WHIP
|
||||
|
||||
You can use **OBS Studio** or any other broadcast software with [WHIP](https://www.ietf.org/archive/id/draft-ietf-wish-whip-01.html) protocol support. This standard has not yet been approved. But you can download OBS Studio [dev version](https://github.com/obsproject/obs-studio/actions/runs/3969201209):
|
||||
|
||||
- Settings > Stream > Service: WHIP > http://192.168.1.123:1984/api/webrtc?dst=camera1
|
||||
|
||||
#### Stream to camera
|
||||
|
||||
go2rtc support play audio files (ex. music or [TTS](https://www.home-assistant.io/integrations/#text-to-speech)) and live streams (ex. radio) on cameras with [two way audio](#two-way-audio) support.
|
||||
go2rtc support play audio files (ex. music or [TTS](https://www.home-assistant.io/integrations/#text-to-speech)) and live streams (ex. radio) on cameras with [two way audio](#two-way-audio) support (RTSP/ONVIF cameras, TP-Link Tapo, Hikvision ISAPI, Roborock vacuums, any Browser).
|
||||
|
||||
API example:
|
||||
|
||||
@@ -606,6 +678,32 @@ webrtc:
|
||||
credential: your_pass
|
||||
```
|
||||
|
||||
### Module: WebTorrent
|
||||
|
||||
This module support:
|
||||
|
||||
- Share any local stream via [WebTorrent](https://webtorrent.io/) technology
|
||||
- Get any [incoming stream](#incoming-browser) from PC or mobile via [WebTorrent](https://webtorrent.io/) technology
|
||||
- Get any remote [go2rtc source](#source-webtorrent) via [WebTorrent](https://webtorrent.io/) technology
|
||||
|
||||
Securely and free. You do not need to open a public access to the go2rtc server. But in some cases (Symmetric NAT) you may need to set up external access to [WebRTC module](#module-webrtc).
|
||||
|
||||
To generate sharing link or incoming link - goto go2rtc WebUI (stream links page). This link is **temporary** and will stop working after go2rtc is restarted!
|
||||
|
||||
You can create permanent external links in go2rtc config:
|
||||
|
||||
```yaml
|
||||
webtorrent:
|
||||
shares:
|
||||
super-secret-share: # share name, should be unique among all go2rtc users!
|
||||
pwd: super-secret-password
|
||||
src: rtsp-dahua1 # stream name from streams section
|
||||
```
|
||||
|
||||
Link example: https://alexxit.github.io/go2rtc/#share=02SNtgjKXY&pwd=wznEQqznxW&media=video+audio
|
||||
|
||||
TODO: article how it works...
|
||||
|
||||
### Module: Ngrok
|
||||
|
||||
With Ngrok integration you can get external access to your streams in situation when you have Internet with private IP-address.
|
||||
|
||||
+9
-5
@@ -54,28 +54,31 @@ func Init() {
|
||||
|
||||
log.Info().Str("addr", cfg.Mod.Listen).Msg("[api] listen")
|
||||
|
||||
s := http.Server{}
|
||||
s.Handler = http.DefaultServeMux // 4th
|
||||
Handler = http.DefaultServeMux // 4th
|
||||
|
||||
if cfg.Mod.Origin == "*" {
|
||||
s.Handler = middlewareCORS(s.Handler) // 3rd
|
||||
Handler = middlewareCORS(Handler) // 3rd
|
||||
}
|
||||
|
||||
if cfg.Mod.Username != "" {
|
||||
s.Handler = middlewareAuth(cfg.Mod.Username, cfg.Mod.Password, s.Handler) // 2nd
|
||||
Handler = middlewareAuth(cfg.Mod.Username, cfg.Mod.Password, Handler) // 2nd
|
||||
}
|
||||
|
||||
if log.Trace().Enabled() {
|
||||
s.Handler = middlewareLog(s.Handler) // 1st
|
||||
Handler = middlewareLog(Handler) // 1st
|
||||
}
|
||||
|
||||
go func() {
|
||||
s := http.Server{}
|
||||
s.Handler = Handler
|
||||
if err = s.Serve(listener); err != nil {
|
||||
log.Fatal().Err(err).Msg("[api] serve")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
var Handler http.Handler
|
||||
|
||||
// HandleFunc handle pattern with relative path:
|
||||
// - "api/streams" => "{basepath}/api/streams"
|
||||
// - "/streams" => "/streams"
|
||||
@@ -118,6 +121,7 @@ func middlewareCORS(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Authorization")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
+14
-5
@@ -2,19 +2,21 @@ package app
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var Version = "1.3.0"
|
||||
var Version = "1.4.0"
|
||||
var UserAgent = "go2rtc/" + Version
|
||||
|
||||
var ConfigPath string
|
||||
@@ -24,10 +26,17 @@ var Info = map[string]any{
|
||||
|
||||
func Init() {
|
||||
var confs Config
|
||||
var version bool
|
||||
|
||||
flag.Var(&confs, "config", "go2rtc config (path to file or raw text), support multiple")
|
||||
flag.BoolVar(&version, "version", false, "Print the version of the application and exit")
|
||||
flag.Parse()
|
||||
|
||||
if version {
|
||||
fmt.Println("Current version: ", Version)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if confs == nil {
|
||||
confs = []string{"go2rtc.yaml"}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,10 @@ import (
|
||||
const deviceInputPrefix = "-f v4l2"
|
||||
|
||||
func deviceInputSuffix(videoIdx, audioIdx int) string {
|
||||
video := findMedia(core.KindVideo, videoIdx)
|
||||
return video.ID
|
||||
if video := findMedia(core.KindVideo, videoIdx); video != nil {
|
||||
return video.ID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func loadMedias() {
|
||||
|
||||
@@ -51,7 +51,7 @@ var defaults = map[string]string{
|
||||
"rtsp/udp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i {input}",
|
||||
|
||||
// output
|
||||
"output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
|
||||
"output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -bufsize 8192k -f rtsp {output}",
|
||||
|
||||
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
|
||||
// `-tune zerolatency` - for minimal latency
|
||||
@@ -60,7 +60,8 @@ var defaults = map[string]string{
|
||||
"h265": "-c:v libx265 -g 50 -profile:v high -level:v 5.1 -preset:v superfast -tune:v zerolatency",
|
||||
"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
|
||||
|
||||
"opus": "-c:a libopus -ar:a 48000 -ac:a 2",
|
||||
// https://ffmpeg.org/ffmpeg-codecs.html#libopus-1
|
||||
"opus": "-c:a libopus -ar:a 48000 -ac:a 2 -application:a voip -compression_level:a 0",
|
||||
"pcmu": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
|
||||
"pcmu/16000": "-c:a pcm_mulaw -ar:a 16000 -ac:a 1",
|
||||
"pcmu/48000": "-c:a pcm_mulaw -ar:a 48000 -ac:a 1",
|
||||
@@ -70,8 +71,7 @@ var defaults = map[string]string{
|
||||
"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",
|
||||
"pcm": "-c:a pcm_s16be",
|
||||
"pcm/8000": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
|
||||
"pcm": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
|
||||
"pcm/16000": "-c:a pcm_s16be -ar:a 16000 -ac:a 1",
|
||||
"pcm/48000": "-c:a pcm_s16be -ar:a 48000 -ac:a 1",
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseArgs(t *testing.T) {
|
||||
args := parseArgs("rtsp://example.com#video=h264#rotate=180")
|
||||
assert.Equal(t, "ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i rtsp://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -an -vf transpose=1,transpose=1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", args.String())
|
||||
|
||||
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=vaapi")
|
||||
assert.Equal(t, "ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i rtsp://example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf format=vaapi|nv12,hwupload,transpose_vaapi=4 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", args.String())
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -54,6 +55,13 @@ func MakeHardware(args *Args, engine string) {
|
||||
if strings.HasPrefix(filter, "scale=") {
|
||||
args.filters[i] = "scale_vaapi=" + filter[6:]
|
||||
}
|
||||
if strings.HasPrefix(filter, "transpose=") {
|
||||
if filter == "transpose=1,transpose=1" { // 180 degrees half-turn
|
||||
args.filters[i] = "transpose_vaapi=4" // reversal
|
||||
} else {
|
||||
args.filters[i] = "transpose_vaapi=" + filter[10:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fix if input doesn't support hwaccel, do nothing when support
|
||||
|
||||
+18
-55
@@ -6,8 +6,8 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/cmd/webrtc"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -24,6 +24,7 @@ func initAPI() {
|
||||
|
||||
api.HandleFunc("/streams", ok)
|
||||
|
||||
// api from RTSPtoWeb
|
||||
api.HandleFunc("/stream/", func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
// /stream/{id}/add
|
||||
@@ -39,13 +40,7 @@ func initAPI() {
|
||||
// 3. dynamic link to Hass camera
|
||||
stream := streams.Get(v.Name)
|
||||
if stream == nil {
|
||||
// check if it is rtsp link to go2rtc
|
||||
stream = rtspStream(v.Channels.First.Url)
|
||||
if stream != nil {
|
||||
streams.New(v.Name, stream)
|
||||
} else {
|
||||
stream = streams.New(v.Name, "{input}")
|
||||
}
|
||||
stream = streams.NewTemplate(v.Name, v.Channels.First.Url)
|
||||
}
|
||||
|
||||
stream.SetSource(v.Channels.First.Url)
|
||||
@@ -89,57 +84,25 @@ func initAPI() {
|
||||
_, _ = w.Write([]byte(s))
|
||||
}
|
||||
})
|
||||
|
||||
// api from RTSPtoWebRTC
|
||||
api.HandleFunc("/stream", func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
str := r.FormValue("sdp64")
|
||||
offer, err := base64.StdEncoding.DecodeString(str)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
src := r.FormValue("url")
|
||||
src, err = url.QueryUnescape(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
stream := streams.Get(src)
|
||||
if stream == nil {
|
||||
if stream = rtspStream(src); stream != nil {
|
||||
streams.New(src, stream)
|
||||
} else {
|
||||
stream = streams.New(src, src)
|
||||
}
|
||||
}
|
||||
|
||||
str, err = webrtc.ExchangeSDP(stream, string(offer), "WebRTC/Hass sync", r.UserAgent())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
v := struct {
|
||||
Answer string `json:"sdp64"`
|
||||
}{
|
||||
Answer: base64.StdEncoding.EncodeToString([]byte(str)),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
})
|
||||
}
|
||||
|
||||
func rtspStream(url string) *streams.Stream {
|
||||
if strings.HasPrefix(url, "rtsp://") {
|
||||
if i := strings.IndexByte(url[7:], '/'); i > 0 {
|
||||
return streams.Get(url[8+i:])
|
||||
func HassioAddr() string {
|
||||
ints, _ := net.Interfaces()
|
||||
|
||||
for _, i := range ints {
|
||||
if i.Name != "hassio" {
|
||||
continue
|
||||
}
|
||||
|
||||
addrs, _ := i.Addrs()
|
||||
for _, addr := range addrs {
|
||||
if addr, ok := addr.(*net.IPNet); ok {
|
||||
return addr.IP.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
type addJSON struct {
|
||||
|
||||
+52
-25
@@ -17,6 +17,9 @@ import (
|
||||
|
||||
func Init() {
|
||||
var conf struct {
|
||||
API struct {
|
||||
Listen string `json:"listen"`
|
||||
} `yaml:"api"`
|
||||
Mod struct {
|
||||
Config string `yaml:"config"`
|
||||
} `yaml:"hass"`
|
||||
@@ -28,35 +31,68 @@ func Init() {
|
||||
|
||||
initAPI()
|
||||
|
||||
// support load cameras from Hass config file
|
||||
filename := path.Join(conf.Mod.Config, ".storage/core.config_entries")
|
||||
b, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
entries := importEntries(conf.Mod.Config)
|
||||
if entries == nil {
|
||||
api.HandleFunc("api/hass", func(w http.ResponseWriter, _ *http.Request) {
|
||||
http.Error(w, "no hass config", http.StatusNotFound)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
storage := new(entries)
|
||||
if err = json.Unmarshal(b, storage); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
urls := map[string]string{}
|
||||
|
||||
api.HandleFunc("api/hass", func(w http.ResponseWriter, r *http.Request) {
|
||||
api.HandleFunc("api/hass", func(w http.ResponseWriter, _ *http.Request) {
|
||||
var items []api.Stream
|
||||
for name, url := range urls {
|
||||
for name, url := range entries {
|
||||
items = append(items, api.Stream{Name: name, URL: url})
|
||||
}
|
||||
api.ResponseStreams(w, items)
|
||||
})
|
||||
|
||||
streams.HandleFunc("hass", func(url string) (core.Producer, error) {
|
||||
if hurl := urls[url[5:]]; hurl != "" {
|
||||
if hurl := entries[url[5:]]; hurl != "" {
|
||||
return streams.GetProducer(hurl)
|
||||
}
|
||||
return nil, fmt.Errorf("can't get url: %s", url)
|
||||
})
|
||||
|
||||
// for Addon listen on hassio interface, so WebUI feature will work
|
||||
if conf.API.Listen == "127.0.0.1:1984" {
|
||||
if addr := HassioAddr(); addr != "" {
|
||||
addr += ":1984"
|
||||
go func() {
|
||||
log.Info().Str("addr", addr).Msg("[hass] listen")
|
||||
if err := http.ListenAndServe(addr, api.Handler); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func importEntries(config string) map[string]string {
|
||||
// support load cameras from Hass config file
|
||||
filename := path.Join(config, ".storage/core.config_entries")
|
||||
b, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var storage struct {
|
||||
Data struct {
|
||||
Entries []struct {
|
||||
Title string `json:"title"`
|
||||
Domain string `json:"domain"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Options json.RawMessage `json:"options"`
|
||||
} `json:"entries"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(b, &storage); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
urls := map[string]string{}
|
||||
|
||||
for _, entrie := range storage.Data.Entries {
|
||||
switch entrie.Domain {
|
||||
case "generic":
|
||||
@@ -102,17 +138,8 @@ func Init() {
|
||||
log.Info().Str("url", "hass:"+entrie.Title).Msg("[hass] load stream")
|
||||
//streams.Get("hass:" + entrie.Title)
|
||||
}
|
||||
|
||||
return urls
|
||||
}
|
||||
|
||||
var log zerolog.Logger
|
||||
|
||||
type entries struct {
|
||||
Data struct {
|
||||
Entries []struct {
|
||||
Title string `json:"title"`
|
||||
Domain string `json:"domain"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Options json.RawMessage `json:"options"`
|
||||
} `json:"entries"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
+24
-6
@@ -7,9 +7,10 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/rs/zerolog/log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -28,6 +29,7 @@ func Init() {
|
||||
|
||||
type Consumer interface {
|
||||
core.Consumer
|
||||
Listen(f core.EventFunc)
|
||||
Init() ([]byte, error)
|
||||
MimeCodecs() string
|
||||
Start()
|
||||
@@ -47,6 +49,9 @@ const keepalive = 5 * time.Second
|
||||
|
||||
var sessions = map[string]*Session{}
|
||||
|
||||
// once I saw 404 on MP4 segment, so better to use mutex
|
||||
var sessionsMu sync.RWMutex
|
||||
|
||||
func handlerStream(w http.ResponseWriter, r *http.Request) {
|
||||
// CORS important for Chromecast
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
@@ -70,20 +75,20 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
|
||||
medias := mp4.ParseQuery(r.URL.Query())
|
||||
if medias != nil {
|
||||
cons = &mp4.Consumer{
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
RemoteAddr: tcp.RemoteAddr(r),
|
||||
UserAgent: r.UserAgent(),
|
||||
Medias: medias,
|
||||
}
|
||||
} else {
|
||||
cons = &mpegts.Consumer{
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
RemoteAddr: tcp.RemoteAddr(r),
|
||||
UserAgent: r.UserAgent(),
|
||||
}
|
||||
}
|
||||
|
||||
session := &Session{cons: cons}
|
||||
|
||||
cons.(any).(*core.Listener).Listen(func(msg any) {
|
||||
cons.Listen(func(msg any) {
|
||||
if data, ok := msg.([]byte); ok {
|
||||
session.mu.Lock()
|
||||
session.segment = append(session.segment, data...)
|
||||
@@ -103,7 +108,7 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
cons.Start()
|
||||
|
||||
sid := strconv.FormatInt(time.Now().UnixNano(), 10)
|
||||
sid := core.RandString(8, 62)
|
||||
|
||||
// two segments important for Chromecast
|
||||
if medias != nil {
|
||||
@@ -127,11 +132,16 @@ segment.ts?id=` + sid + `&n=%d
|
||||
segment.ts?id=` + sid + `&n=%d`
|
||||
}
|
||||
|
||||
sessionsMu.Lock()
|
||||
sessions[sid] = session
|
||||
sessionsMu.Unlock()
|
||||
|
||||
// Apple Safari can play FLAC codec, but fail it it in m3u8 playlist
|
||||
codecs := strings.Replace(cons.MimeCodecs(), mp4.MimeFlac, mp4.MimeAAC, 1)
|
||||
|
||||
// bandwidth important for Safari, codecs useful for smooth playback
|
||||
data := []byte(`#EXTM3U
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS="` + cons.MimeCodecs() + `"
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS="` + codecs + `"
|
||||
hls/playlist.m3u8?id=` + sid)
|
||||
|
||||
if _, err := w.Write(data); err != nil {
|
||||
@@ -149,7 +159,9 @@ func handlerPlaylist(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
sid := r.URL.Query().Get("id")
|
||||
sessionsMu.RLock()
|
||||
session := sessions[sid]
|
||||
sessionsMu.RUnlock()
|
||||
if session == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
@@ -172,7 +184,9 @@ func handlerSegmentTS(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
sid := r.URL.Query().Get("id")
|
||||
sessionsMu.RLock()
|
||||
session := sessions[sid]
|
||||
sessionsMu.RUnlock()
|
||||
if session == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
@@ -211,7 +225,9 @@ func handlerInit(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
sid := r.URL.Query().Get("id")
|
||||
sessionsMu.RLock()
|
||||
session := sessions[sid]
|
||||
sessionsMu.RUnlock()
|
||||
if session == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
@@ -232,7 +248,9 @@ func handlerSegmentMP4(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
sid := r.URL.Query().Get("id")
|
||||
sessionsMu.RLock()
|
||||
session := sessions[sid]
|
||||
sessionsMu.RUnlock()
|
||||
if session == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
|
||||
+4
-3
@@ -5,6 +5,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/rs/zerolog/log"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -29,7 +30,7 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
||||
exit := make(chan []byte)
|
||||
|
||||
cons := &mjpeg.Consumer{
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
RemoteAddr: tcp.RemoteAddr(r),
|
||||
UserAgent: r.UserAgent(),
|
||||
}
|
||||
cons.Listen(func(msg any) {
|
||||
@@ -81,7 +82,7 @@ func outputMjpeg(w http.ResponseWriter, r *http.Request) {
|
||||
flusher := w.(http.Flusher)
|
||||
|
||||
cons := &mjpeg.Consumer{
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
RemoteAddr: tcp.RemoteAddr(r),
|
||||
UserAgent: r.UserAgent(),
|
||||
}
|
||||
cons.Listen(func(msg any) {
|
||||
@@ -146,7 +147,7 @@ func handlerWS(tr *api.Transport, _ *api.Message) error {
|
||||
}
|
||||
|
||||
cons := &mjpeg.Consumer{
|
||||
RemoteAddr: tr.Request.RemoteAddr,
|
||||
RemoteAddr: tcp.RemoteAddr(tr.Request),
|
||||
UserAgent: tr.Request.UserAgent(),
|
||||
}
|
||||
cons.Listen(func(msg any) {
|
||||
|
||||
+38
-25
@@ -1,16 +1,17 @@
|
||||
package mp4
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||
"github.com/rs/zerolog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
@@ -43,18 +44,22 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
exit := make(chan []byte)
|
||||
exit := make(chan []byte, 1)
|
||||
|
||||
cons := &mp4.Segment{OnlyKeyframe: true}
|
||||
cons.Listen(func(msg any) {
|
||||
if data, ok := msg.([]byte); ok && exit != nil {
|
||||
exit <- data
|
||||
select {
|
||||
case exit <- data:
|
||||
default:
|
||||
}
|
||||
exit = nil
|
||||
}
|
||||
})
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -74,18 +79,13 @@ 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)
|
||||
|
||||
// Chrome has Safari in UA, so check first Chrome and later Safari
|
||||
query := r.URL.Query()
|
||||
|
||||
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/") {
|
||||
if strings.Contains(ua, " Safari/") && !strings.Contains(ua, " Chrome/") && !query.Has("duration") {
|
||||
// auto redirect to HLS/fMP4 format, because Safari not support MP4 stream
|
||||
url := "stream.m3u8?" + r.URL.RawQuery
|
||||
if !r.URL.Query().Has("mp4") {
|
||||
if !query.Has("mp4") {
|
||||
url += "&mp4"
|
||||
}
|
||||
|
||||
@@ -93,25 +93,31 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
src := r.URL.Query().Get("src")
|
||||
src := query.Get("src")
|
||||
stream := streams.GetOrNew(src)
|
||||
if stream == nil {
|
||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
exit := make(chan error)
|
||||
exit := make(chan error, 1) // Add buffer to prevent blocking
|
||||
|
||||
cons := &mp4.Consumer{
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
RemoteAddr: tcp.RemoteAddr(r),
|
||||
UserAgent: r.UserAgent(),
|
||||
Medias: core.ParseQuery(r.URL.Query()),
|
||||
Medias: mp4.ParseQuery(r.URL.Query()),
|
||||
}
|
||||
|
||||
cons.Listen(func(msg any) {
|
||||
if exit == nil {
|
||||
return
|
||||
}
|
||||
if data, ok := msg.([]byte); ok {
|
||||
if _, err := w.Write(data); err != nil && exit != nil {
|
||||
exit <- err
|
||||
if _, err := w.Write(data); err != nil {
|
||||
select {
|
||||
case exit <- err:
|
||||
default:
|
||||
}
|
||||
exit = nil
|
||||
}
|
||||
}
|
||||
@@ -119,6 +125,7 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -129,22 +136,27 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := cons.Init()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = w.Write(data); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
cons.Start()
|
||||
|
||||
var duration *time.Timer
|
||||
if s := r.URL.Query().Get("duration"); s != "" {
|
||||
if s := query.Get("duration"); s != "" {
|
||||
if i, _ := strconv.Atoi(s); i > 0 {
|
||||
duration = time.AfterFunc(time.Second*time.Duration(i), func() {
|
||||
if exit != nil {
|
||||
exit <- nil
|
||||
select {
|
||||
case exit <- nil:
|
||||
default:
|
||||
}
|
||||
exit = nil
|
||||
}
|
||||
})
|
||||
@@ -152,6 +164,7 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
err = <-exit
|
||||
exit = nil
|
||||
|
||||
log.Trace().Err(err).Caller().Send()
|
||||
|
||||
|
||||
+9
-2
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -17,7 +18,7 @@ func handlerWSMSE(tr *api.Transport, msg *api.Message) error {
|
||||
}
|
||||
|
||||
cons := &mp4.Consumer{
|
||||
RemoteAddr: tr.Request.RemoteAddr,
|
||||
RemoteAddr: tcp.RemoteAddr(tr.Request),
|
||||
UserAgent: tr.Request.UserAgent(),
|
||||
}
|
||||
|
||||
@@ -64,7 +65,7 @@ func handlerWSMP4(tr *api.Transport, msg *api.Message) error {
|
||||
}
|
||||
|
||||
cons := &mp4.Segment{
|
||||
RemoteAddr: tr.Request.RemoteAddr,
|
||||
RemoteAddr: tcp.RemoteAddr(tr.Request),
|
||||
UserAgent: tr.Request.UserAgent(),
|
||||
OnlyKeyframe: true,
|
||||
}
|
||||
@@ -109,6 +110,12 @@ func parseMedias(codecs string, parseAudio bool) (medias []*core.Media) {
|
||||
case mp4.MimeAAC:
|
||||
codec := &core.Codec{Name: core.CodecAAC}
|
||||
audios = append(audios, codec)
|
||||
case mp4.MimeFlac:
|
||||
audios = append(audios,
|
||||
&core.Codec{Name: core.CodecPCMA},
|
||||
&core.Codec{Name: core.CodecPCMU},
|
||||
&core.Codec{Name: core.CodecPCM},
|
||||
)
|
||||
case mp4.MimeOpus:
|
||||
codec := &core.Codec{Name: core.CodecOpus}
|
||||
audios = append(audios, codec)
|
||||
|
||||
+9
-4
@@ -1,6 +1,11 @@
|
||||
package rtsp
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
@@ -8,9 +13,6 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/rs/zerolog"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
@@ -123,6 +125,7 @@ func rtspHandler(url string) (core.Producer, error) {
|
||||
if !backchannel {
|
||||
return nil, err
|
||||
}
|
||||
log.Trace().Msgf("[rtsp] describe (backchannel=%t) err: %v", backchannel, err)
|
||||
|
||||
// second try without backchannel, we need to reconnect
|
||||
conn.Backchannel = false
|
||||
@@ -211,7 +214,9 @@ func tcpHandler(conn *rtsp.Conn) {
|
||||
})
|
||||
|
||||
if err := conn.Accept(); err != nil {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
if err != io.EOF {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
}
|
||||
if closer != nil {
|
||||
closer()
|
||||
}
|
||||
|
||||
+20
-4
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/app/store"
|
||||
"github.com/rs/zerolog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
@@ -39,6 +40,20 @@ func New(name string, source any) *Stream {
|
||||
return stream
|
||||
}
|
||||
|
||||
func NewTemplate(name string, source any) *Stream {
|
||||
// check if source links to some stream name from go2rtc
|
||||
if rawURL, ok := source.(string); ok {
|
||||
if u, err := url.Parse(rawURL); err == nil && u.Scheme == "rtsp" {
|
||||
if stream, ok := streams[u.Path[1:]]; ok {
|
||||
streams[name] = stream
|
||||
return stream
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return New(name, "{input}")
|
||||
}
|
||||
|
||||
func GetOrNew(src string) *Stream {
|
||||
if stream, ok := streams[src]; ok {
|
||||
return stream
|
||||
@@ -85,11 +100,12 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if stream := Get(name); stream != nil {
|
||||
stream.SetSource(src)
|
||||
} else {
|
||||
New(name, src)
|
||||
// support {input} templates: https://github.com/AlexxIT/go2rtc#module-hass
|
||||
stream := Get(name)
|
||||
if stream == nil {
|
||||
stream = NewTemplate(name, src)
|
||||
}
|
||||
stream.SetSource(src)
|
||||
|
||||
case "POST":
|
||||
// with dst - redirect source to dst
|
||||
|
||||
+23
-13
@@ -30,8 +30,6 @@ type Producer struct {
|
||||
receivers []*core.Receiver
|
||||
senders []*core.Receiver
|
||||
|
||||
lastErr error
|
||||
|
||||
state state
|
||||
mu sync.Mutex
|
||||
workerID int
|
||||
@@ -157,10 +155,10 @@ func (p *Producer) worker(conn core.Producer, workerID int) {
|
||||
log.Warn().Err(err).Str("url", p.url).Caller().Send()
|
||||
}
|
||||
|
||||
p.reconnect(workerID)
|
||||
p.reconnect(workerID, 0)
|
||||
}
|
||||
|
||||
func (p *Producer) reconnect(workerID int) {
|
||||
func (p *Producer) reconnect(workerID, retry int) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
@@ -169,18 +167,28 @@ func (p *Producer) reconnect(workerID int) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Msgf("[streams] reconnect to url=%s", p.url)
|
||||
log.Debug().Msgf("[streams] retry=%d to url=%s", retry, p.url)
|
||||
|
||||
if err := p.Dial(); err != nil {
|
||||
conn, err := GetProducer(p.url)
|
||||
if err != nil {
|
||||
log.Debug().Msgf("[streams] producer=%s", err)
|
||||
// TODO: dynamic timeout
|
||||
time.AfterFunc(30*time.Second, func() {
|
||||
p.reconnect(workerID)
|
||||
|
||||
timeout := time.Minute
|
||||
if retry < 5 {
|
||||
timeout = time.Second
|
||||
} else if retry < 10 {
|
||||
timeout = time.Second * 5
|
||||
} else if retry < 20 {
|
||||
timeout = time.Second * 10
|
||||
}
|
||||
|
||||
time.AfterFunc(timeout, func() {
|
||||
p.reconnect(workerID, retry+1)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
for _, media := range p.conn.GetMedias() {
|
||||
for _, media := range conn.GetMedias() {
|
||||
switch media.Direction {
|
||||
case core.DirectionRecvonly:
|
||||
for _, receiver := range p.receivers {
|
||||
@@ -189,7 +197,7 @@ func (p *Producer) reconnect(workerID int) {
|
||||
continue
|
||||
}
|
||||
|
||||
track, err := p.conn.GetTrack(media, codec)
|
||||
track, err := conn.GetTrack(media, codec)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -205,12 +213,14 @@ func (p *Producer) reconnect(workerID int) {
|
||||
continue
|
||||
}
|
||||
|
||||
_ = p.conn.(core.Consumer).AddTrack(media, codec, sender)
|
||||
_ = conn.(core.Consumer).AddTrack(media, codec, sender)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
go p.worker(p.conn, workerID)
|
||||
p.conn = conn
|
||||
|
||||
go p.worker(conn, workerID)
|
||||
}
|
||||
|
||||
func (p *Producer) stop() {
|
||||
|
||||
+61
-36
@@ -3,7 +3,6 @@ package streams
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -31,8 +30,6 @@ func NewStream(source any) *Stream {
|
||||
s.producers = append(s.producers, prod)
|
||||
}
|
||||
return s
|
||||
case *Stream:
|
||||
return source
|
||||
case map[string]any:
|
||||
return NewStream(source["url"])
|
||||
case nil:
|
||||
@@ -50,24 +47,28 @@ func (s *Stream) SetSource(source string) {
|
||||
|
||||
func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
|
||||
// support for multiple simultaneous requests from different consumers
|
||||
atomic.AddInt32(&s.requests, 1)
|
||||
consN := atomic.AddInt32(&s.requests, 1) - 1
|
||||
|
||||
var producers []*Producer // matched producers for consumer
|
||||
|
||||
var codecs string
|
||||
var statErrors []error
|
||||
var statMedias []*core.Media
|
||||
var statProds []*Producer // matched producers for consumer
|
||||
|
||||
// Step 1. Get consumer medias
|
||||
for _, consMedia := range cons.GetMedias() {
|
||||
log.Trace().Msgf("[streams] check cons=%d media=%s", consN, consMedia)
|
||||
|
||||
producers:
|
||||
for _, prod := range s.producers {
|
||||
for prodN, prod := range s.producers {
|
||||
if err = prod.Dial(); err != nil {
|
||||
log.Trace().Err(err).Msgf("[streams] skip prod=%s", prod.url)
|
||||
statErrors = append(statErrors, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Step 2. Get producer medias (not tracks yet)
|
||||
for _, prodMedia := range prod.GetMedias() {
|
||||
collectCodecs(prodMedia, &codecs)
|
||||
log.Trace().Msgf("[streams] check prod=%d media=%s", prodN, prodMedia)
|
||||
statMedias = append(statMedias, prodMedia)
|
||||
|
||||
// Step 3. Match consumer/producer codecs list
|
||||
prodCodec, consCodec := prodMedia.MatchMedia(consMedia)
|
||||
@@ -79,6 +80,8 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
|
||||
|
||||
switch prodMedia.Direction {
|
||||
case core.DirectionRecvonly:
|
||||
log.Trace().Msgf("[streams] match prod=%d => cons=%d", prodN, consN)
|
||||
|
||||
// Step 4. Get recvonly track from producer
|
||||
if track, err = prod.GetTrack(prodMedia, prodCodec); err != nil {
|
||||
log.Info().Err(err).Msg("[streams] can't get track")
|
||||
@@ -91,6 +94,8 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
|
||||
}
|
||||
|
||||
case core.DirectionSendonly:
|
||||
log.Trace().Msgf("[streams] match cons=%d => prod=%d", consN, prodN)
|
||||
|
||||
// Step 4. Get recvonly track from consumer (backchannel)
|
||||
if track, err = cons.(core.Producer).GetTrack(consMedia, consCodec); err != nil {
|
||||
log.Info().Err(err).Msg("[streams] can't get track")
|
||||
@@ -103,7 +108,7 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
producers = append(producers, prod)
|
||||
statProds = append(statProds, prod)
|
||||
|
||||
if !consMedia.MatchAll() {
|
||||
break producers
|
||||
@@ -117,18 +122,8 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
|
||||
s.stopProducers()
|
||||
}
|
||||
|
||||
if len(producers) == 0 {
|
||||
if len(codecs) > 0 {
|
||||
return errors.New("codecs not match: " + codecs)
|
||||
}
|
||||
|
||||
for i, producer := range s.producers {
|
||||
if producer.lastErr != nil {
|
||||
return fmt.Errorf("source %d error: %w", i, producer.lastErr)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("sources unavailable: %d", len(s.producers))
|
||||
if len(statProds) == 0 {
|
||||
return formatError(statMedias, statErrors)
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
@@ -136,7 +131,7 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
|
||||
s.mu.Unlock()
|
||||
|
||||
// there may be duplicates, but that's not a problem
|
||||
for _, prod := range producers {
|
||||
for _, prod := range statProds {
|
||||
prod.start()
|
||||
}
|
||||
|
||||
@@ -185,6 +180,11 @@ producers:
|
||||
continue producers
|
||||
}
|
||||
}
|
||||
for _, track := range producer.senders {
|
||||
if len(track.Senders()) > 0 {
|
||||
continue producers
|
||||
}
|
||||
}
|
||||
producer.stop()
|
||||
}
|
||||
s.mu.Unlock()
|
||||
@@ -208,22 +208,47 @@ func (s *Stream) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(info)
|
||||
}
|
||||
|
||||
func collectCodecs(media *core.Media, codecs *string) {
|
||||
if media.Direction == core.DirectionRecvonly {
|
||||
return
|
||||
}
|
||||
func formatError(statMedias []*core.Media, statErrors []error) error {
|
||||
var text string
|
||||
|
||||
for _, codec := range media.Codecs {
|
||||
name := codec.Name
|
||||
if name == core.CodecAAC {
|
||||
name = "AAC"
|
||||
}
|
||||
if strings.Contains(*codecs, name) {
|
||||
for _, media := range statMedias {
|
||||
if media.Direction == core.DirectionRecvonly {
|
||||
continue
|
||||
}
|
||||
if len(*codecs) > 0 {
|
||||
*codecs += ","
|
||||
|
||||
for _, codec := range media.Codecs {
|
||||
name := codec.Name
|
||||
if name == core.CodecAAC {
|
||||
name = "AAC"
|
||||
}
|
||||
if strings.Contains(text, name) {
|
||||
continue
|
||||
}
|
||||
if len(text) > 0 {
|
||||
text += ","
|
||||
}
|
||||
text += name
|
||||
}
|
||||
*codecs += name
|
||||
}
|
||||
|
||||
if text != "" {
|
||||
return errors.New(text)
|
||||
}
|
||||
|
||||
for _, err := range statErrors {
|
||||
s := err.Error()
|
||||
if strings.Contains(text, s) {
|
||||
continue
|
||||
}
|
||||
if len(text) > 0 {
|
||||
text += ","
|
||||
}
|
||||
text += s
|
||||
}
|
||||
|
||||
if text != "" {
|
||||
return errors.New(text)
|
||||
}
|
||||
|
||||
return errors.New("unknown error")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package streams
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTemplate(t *testing.T) {
|
||||
source1 := "does not matter"
|
||||
|
||||
stream1 := New("from_yaml", source1)
|
||||
require.Len(t, streams, 1)
|
||||
|
||||
stream2 := NewTemplate("camera.from_hass", "rtsp://localhost:8554/from_yaml?video")
|
||||
|
||||
require.Equal(t, stream1, stream2)
|
||||
require.Equal(t, stream2.producers[0].url, source1)
|
||||
require.Len(t, streams, 2)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("tcp", handle)
|
||||
}
|
||||
|
||||
func handle(rawURL string) (core.Producer, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn, err := net.DialTimeout("tcp", u.Host, time.Second*3)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := &http.Request{URL: u}
|
||||
res := &http.Response{Body: conn, Request: req}
|
||||
client := mpegts.NewClient(res)
|
||||
if err := client.Handle(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
+41
-7
@@ -201,13 +201,17 @@ func ExchangeSDP(stream *streams.Stream, offer, desc, userAgent string) (answer
|
||||
// create new webrtc instance
|
||||
conn := webrtc.NewConn(pc)
|
||||
conn.Desc = desc
|
||||
conn.Mode = core.ModePassiveConsumer
|
||||
conn.UserAgent = userAgent
|
||||
conn.Listen(func(msg any) {
|
||||
switch msg := msg.(type) {
|
||||
case pion.PeerConnectionState:
|
||||
if msg == pion.PeerConnectionStateClosed {
|
||||
if msg != pion.PeerConnectionStateClosed {
|
||||
return
|
||||
}
|
||||
if conn.Mode == core.ModePassiveConsumer {
|
||||
stream.RemoveConsumer(conn)
|
||||
} else {
|
||||
stream.RemoveProducer(conn)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -220,11 +224,19 @@ func ExchangeSDP(stream *streams.Stream, offer, desc, userAgent string) (answer
|
||||
return
|
||||
}
|
||||
|
||||
// 2. AddConsumer, so we get new tracks
|
||||
if err = stream.AddConsumer(conn); err != nil {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
_ = conn.Close()
|
||||
return
|
||||
if IsConsumer(conn) {
|
||||
conn.Mode = core.ModePassiveConsumer
|
||||
|
||||
// 2. AddConsumer, so we get new tracks
|
||||
if err = stream.AddConsumer(conn); err != nil {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
conn.Mode = core.ModePassiveProducer
|
||||
|
||||
stream.AddProducer(conn)
|
||||
}
|
||||
|
||||
answer, err = conn.GetCompleteAnswer()
|
||||
@@ -239,3 +251,25 @@ func ExchangeSDP(stream *streams.Stream, offer, desc, userAgent string) (answer
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func IsConsumer(conn *webrtc.Conn) bool {
|
||||
// if wants get video - consumer
|
||||
for _, media := range conn.GetMedias() {
|
||||
if media.Kind == core.KindVideo && media.Direction == core.DirectionSendonly {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// if wants send video - producer
|
||||
for _, media := range conn.GetMedias() {
|
||||
if media.Kind == core.KindVideo && media.Direction == core.DirectionRecvonly {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// if wants something - consumer
|
||||
for _, media := range conn.GetMedias() {
|
||||
if media.Direction == core.DirectionSendonly {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ require (
|
||||
github.com/pion/stun v0.4.0
|
||||
github.com/pion/webrtc/v3 v3.1.58
|
||||
github.com/rs/zerolog v1.29.0
|
||||
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3
|
||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
|
||||
github.com/stretchr/testify v1.8.2
|
||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
|
||||
@@ -106,6 +106,10 @@ github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
|
||||
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs=
|
||||
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
|
||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA=
|
||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
|
||||
+17
-13
@@ -1,7 +1,9 @@
|
||||
# syntax=docker/dockerfile:labs
|
||||
|
||||
# 0. Prepare images
|
||||
# only debian 12 (bookworm) has latest ffmpeg
|
||||
ARG DEBIAN_VERSION="bookworm-slim"
|
||||
ARG GO_VERSION="1.19-buster"
|
||||
ARG GO_VERSION="1.20-buster"
|
||||
ARG NGROK_VERSION="3"
|
||||
|
||||
FROM debian:${DEBIAN_VERSION} AS base
|
||||
@@ -16,37 +18,39 @@ WORKDIR /build
|
||||
|
||||
# Cache dependencies
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build go mod download
|
||||
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
||||
|
||||
|
||||
# 2. Collect all files
|
||||
FROM scratch AS rootfs
|
||||
|
||||
COPY --from=build /build/go2rtc /usr/local/bin/
|
||||
COPY --from=ngrok /bin/ngrok /usr/local/bin/
|
||||
COPY ./build/docker/run.sh /
|
||||
|
||||
COPY --link --from=build /build/go2rtc /usr/local/bin/
|
||||
COPY --link --from=ngrok /bin/ngrok /usr/local/bin/
|
||||
|
||||
# 3. Final image
|
||||
FROM base
|
||||
|
||||
# Prepare apt for buildkit cache
|
||||
RUN rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache
|
||||
# Install ffmpeg, bash (for run.sh), tini (for signal handling),
|
||||
# and other common tools for the echo source.
|
||||
# non-free for Intel QSV support (not used by go2rtc, just for tests)
|
||||
RUN echo 'deb http://deb.debian.org/debian bookworm non-free' > /etc/apt/sources.list.d/debian-non-free.list && \
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
echo 'deb http://deb.debian.org/debian bookworm non-free' > /etc/apt/sources.list.d/debian-non-free.list && \
|
||||
apt-get -y update && apt-get -y install tini ffmpeg python3 curl jq intel-media-va-driver-non-free
|
||||
|
||||
COPY --from=rootfs / /
|
||||
COPY --link --from=rootfs / /
|
||||
|
||||
|
||||
RUN chmod a+x /run.sh && mkdir -p /config
|
||||
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
|
||||
VOLUME /config
|
||||
WORKDIR /config
|
||||
# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
|
||||
ENV NVIDIA_VISIBLE_DEVICES all
|
||||
ENV NVIDIA_DRIVER_CAPABILITIES compute,video,utility
|
||||
|
||||
CMD ["/run.sh"]
|
||||
CMD ["go2rtc", "-config", "/config/go2rtc.yaml"]
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/srtp"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/cmd/tapo"
|
||||
"github.com/AlexxIT/go2rtc/cmd/tcp"
|
||||
"github.com/AlexxIT/go2rtc/cmd/webrtc"
|
||||
"github.com/AlexxIT/go2rtc/cmd/webtorrent"
|
||||
"os"
|
||||
@@ -49,6 +50,7 @@ func main() {
|
||||
isapi.Init()
|
||||
mpegts.Init()
|
||||
roborock.Init()
|
||||
tcp.Init()
|
||||
|
||||
srtp.Init()
|
||||
homekit.Init()
|
||||
|
||||
+8
-1
@@ -99,9 +99,16 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
|
||||
case "8":
|
||||
c.Name = CodecPCMA
|
||||
c.ClockRate = 8000
|
||||
case "10":
|
||||
c.Name = CodecPCM
|
||||
c.ClockRate = 44100
|
||||
c.Channels = 2
|
||||
case "11":
|
||||
c.Name = CodecPCM
|
||||
c.ClockRate = 44100
|
||||
case "14":
|
||||
c.Name = CodecMP3
|
||||
c.ClockRate = 44100
|
||||
c.ClockRate = 90000 // it's not real sample rate
|
||||
case "26":
|
||||
c.Name = CodecJPEG
|
||||
c.ClockRate = 90000
|
||||
|
||||
+2
-1
@@ -27,7 +27,8 @@ const (
|
||||
CodecMP3 = "MPA" // payload: 14, aka MPEG-1 Layer III
|
||||
CodecPCM = "L16" // Linear PCM
|
||||
|
||||
CodecELD = "ELD" // AAC-ELD
|
||||
CodecELD = "ELD" // AAC-ELD
|
||||
CodecFLAC = "FLAC"
|
||||
|
||||
CodecAll = "ALL"
|
||||
CodecAny = "ANY"
|
||||
|
||||
+12
-1
@@ -82,11 +82,18 @@ func (m *Media) MatchAll() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Media) Equal(media *Media) bool {
|
||||
if media.ID != "" {
|
||||
return m.ID == media.ID
|
||||
}
|
||||
return m.String() == media.String()
|
||||
}
|
||||
|
||||
func GetKind(name string) string {
|
||||
switch name {
|
||||
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG:
|
||||
return KindVideo
|
||||
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecELD:
|
||||
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecPCM, CodecELD, CodecFLAC:
|
||||
return KindAudio
|
||||
}
|
||||
return ""
|
||||
@@ -129,6 +136,10 @@ func MarshalSDP(name string, medias []*Media) ([]byte, error) {
|
||||
}
|
||||
md.WithCodec(codec.PayloadType, name, codec.ClockRate, codec.Channels, codec.FmtpLine)
|
||||
|
||||
if media.ID != "" {
|
||||
md.WithValueAttribute("control", media.ID)
|
||||
}
|
||||
|
||||
sd.MediaDescriptions = append(sd.MediaDescriptions, md)
|
||||
}
|
||||
|
||||
|
||||
+15
-20
@@ -18,7 +18,7 @@ type Receiver struct {
|
||||
ID byte // Channel for RTSP, PayloadType for MPEG-TS
|
||||
|
||||
senders map[*Sender]chan *rtp.Packet
|
||||
mu sync.Mutex
|
||||
mu sync.RWMutex
|
||||
bytes int
|
||||
}
|
||||
|
||||
@@ -32,9 +32,9 @@ func (t *Receiver) WriteRTP(packet *rtp.Packet) {
|
||||
t.mu.Lock()
|
||||
t.bytes += len(packet.Payload)
|
||||
for sender, buffer := range t.senders {
|
||||
if len(buffer) < cap(buffer) {
|
||||
buffer <- packet
|
||||
} else {
|
||||
select {
|
||||
case buffer <- packet:
|
||||
default:
|
||||
sender.overflow++
|
||||
}
|
||||
}
|
||||
@@ -42,11 +42,11 @@ func (t *Receiver) WriteRTP(packet *rtp.Packet) {
|
||||
}
|
||||
|
||||
func (t *Receiver) Senders() (senders []*Sender) {
|
||||
t.mu.Lock()
|
||||
t.mu.RLock()
|
||||
for sender := range t.senders {
|
||||
senders = append(senders, sender)
|
||||
}
|
||||
t.mu.Unlock()
|
||||
t.mu.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -73,12 +73,9 @@ func (t *Receiver) Replace(target *Receiver) {
|
||||
|
||||
func (t *Receiver) String() string {
|
||||
s := t.Codec.String() + ", bytes=" + strconv.Itoa(t.bytes)
|
||||
if t.mu.TryLock() {
|
||||
s += fmt.Sprintf(", senders=%d", len(t.senders))
|
||||
t.mu.Unlock()
|
||||
} else {
|
||||
s += fmt.Sprintf(", senders=?")
|
||||
}
|
||||
t.mu.RLock()
|
||||
s += fmt.Sprintf(", senders=%d", len(t.senders))
|
||||
t.mu.RUnlock()
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -93,7 +90,7 @@ type Sender struct {
|
||||
Handler HandlerFunc
|
||||
|
||||
receivers []*Receiver
|
||||
mu sync.Mutex
|
||||
mu sync.RWMutex
|
||||
bytes int
|
||||
|
||||
overflow int
|
||||
@@ -127,7 +124,6 @@ func (s *Sender) HandleRTP(track *Receiver) {
|
||||
}
|
||||
track.senders[s] = buffer
|
||||
track.mu.Unlock()
|
||||
|
||||
s.mu.Lock()
|
||||
s.receivers = append(s.receivers, track)
|
||||
s.mu.Unlock()
|
||||
@@ -135,7 +131,9 @@ func (s *Sender) HandleRTP(track *Receiver) {
|
||||
go func() {
|
||||
// read packets from buffer channel until it will be closed
|
||||
for packet := range buffer {
|
||||
s.mu.Lock()
|
||||
s.bytes += len(packet.Payload)
|
||||
s.mu.Unlock()
|
||||
s.Handler(packet)
|
||||
}
|
||||
|
||||
@@ -171,12 +169,9 @@ func (s *Sender) Close() {
|
||||
|
||||
func (s *Sender) String() string {
|
||||
info := s.Codec.String() + ", bytes=" + strconv.Itoa(s.bytes)
|
||||
if s.mu.TryLock() {
|
||||
info += ", receivers=" + strconv.Itoa(len(s.receivers))
|
||||
s.mu.Unlock()
|
||||
} else {
|
||||
info += ", receivers=?"
|
||||
}
|
||||
s.mu.RLock()
|
||||
info += ", receivers=" + strconv.Itoa(len(s.receivers))
|
||||
s.mu.RUnlock()
|
||||
if s.overflow > 0 {
|
||||
info += ", overflow=" + strconv.Itoa(s.overflow)
|
||||
}
|
||||
|
||||
+15
-6
@@ -32,6 +32,16 @@ const (
|
||||
Mdat = "mdat"
|
||||
)
|
||||
|
||||
const (
|
||||
sampleIsNonSync = 0x10000
|
||||
sampleDependsOn1 = 0x1000000
|
||||
sampleDependsOn2 = 0x2000000
|
||||
|
||||
SampleVideoIFrame = sampleDependsOn2
|
||||
SampleVideoNonIFrame = sampleDependsOn1 | sampleIsNonSync
|
||||
SampleAudio = sampleIsNonSync
|
||||
)
|
||||
|
||||
func (m *Movie) WriteFileType() {
|
||||
m.StartAtom(Ftyp)
|
||||
m.WriteString("iso5")
|
||||
@@ -250,7 +260,7 @@ func (m *Movie) WriteAudioTrack(id uint32, codec string, timescale uint32, chann
|
||||
m.EndAtom() // TRAK
|
||||
}
|
||||
|
||||
func (m *Movie) WriteMovieFragment(seq, tid, duration, size uint32, time uint64) {
|
||||
func (m *Movie) WriteMovieFragment(seq, tid, duration, size, flags uint32, time uint64) {
|
||||
m.StartAtom(Moof)
|
||||
|
||||
m.StartAtom(MoofMfhd)
|
||||
@@ -276,10 +286,10 @@ func (m *Movie) WriteMovieFragment(seq, tid, duration, size uint32, time uint64)
|
||||
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.WriteUint32(tid) // track id
|
||||
m.WriteUint32(duration) // default sample duration
|
||||
m.WriteUint32(size) // default sample size
|
||||
m.WriteUint32(flags) // default sample flags
|
||||
m.EndAtom()
|
||||
|
||||
m.StartAtom(MoofTrafTfdt)
|
||||
@@ -314,5 +324,4 @@ func (m *Movie) WriteData(b []byte) {
|
||||
m.StartAtom(Mdat)
|
||||
m.Write(b)
|
||||
m.EndAtom()
|
||||
|
||||
}
|
||||
|
||||
+16
-2
@@ -2,6 +2,7 @@ package iso
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/pcm"
|
||||
)
|
||||
|
||||
func (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) {
|
||||
@@ -46,9 +47,11 @@ func (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) {
|
||||
func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, conf []byte) {
|
||||
switch codec {
|
||||
case core.CodecAAC, core.CodecMP3:
|
||||
m.StartAtom("mp4a")
|
||||
m.StartAtom("mp4a") // supported in all players and browsers
|
||||
case core.CodecFLAC:
|
||||
m.StartAtom("fLaC") // supported in all players and browsers
|
||||
case core.CodecOpus:
|
||||
m.StartAtom("Opus")
|
||||
m.StartAtom("Opus") // supported in Chrome and Firefox
|
||||
case core.CodecPCMU:
|
||||
m.StartAtom("ulaw")
|
||||
case core.CodecPCMA:
|
||||
@@ -56,6 +59,11 @@ func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, con
|
||||
default:
|
||||
panic("unsupported iso audio: " + codec)
|
||||
}
|
||||
|
||||
if channels == 0 {
|
||||
channels = 1
|
||||
}
|
||||
|
||||
m.Skip(6)
|
||||
m.WriteUint16(1) // data_reference_index
|
||||
m.Skip(2) // version
|
||||
@@ -72,6 +80,10 @@ func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, con
|
||||
m.WriteEsdsAAC(conf)
|
||||
case core.CodecMP3:
|
||||
m.WriteEsdsMP3()
|
||||
case core.CodecFLAC:
|
||||
m.StartAtom("dfLa")
|
||||
m.Write(pcm.FLACHeader(false, sampleRate))
|
||||
m.EndAtom()
|
||||
case core.CodecOpus:
|
||||
// don't know what means this magic
|
||||
m.StartAtom("dOps")
|
||||
@@ -106,6 +118,7 @@ func (m *Movie) WriteEsdsAAC(conf []byte) {
|
||||
m.Skip(2) // es id
|
||||
m.Skip(1) // es flags
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/medfound/mpeg-4-file-sink#aac-audio
|
||||
m.WriteBytes(4, 0x80, 0x80, 0x80, size4+header+size5)
|
||||
m.WriteBytes(0x40) // object id
|
||||
m.WriteBytes(0x15) // stream type
|
||||
@@ -139,6 +152,7 @@ func (m *Movie) WriteEsdsMP3() {
|
||||
m.Skip(2) // es id
|
||||
m.Skip(1) // es flags
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/medfound/mpeg-4-file-sink#mp3-audio
|
||||
m.WriteBytes(4, 0x80, 0x80, 0x80, size4)
|
||||
m.WriteBytes(0x6B) // object id
|
||||
m.WriteBytes(0x15) // stream type
|
||||
|
||||
+40
-32
@@ -6,7 +6,9 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/AlexxIT/go2rtc/pkg/pcm"
|
||||
"github.com/pion/rtp"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
@@ -19,6 +21,7 @@ type Consumer struct {
|
||||
senders []*core.Sender
|
||||
|
||||
muxer *Muxer
|
||||
mu sync.Mutex
|
||||
wait byte
|
||||
|
||||
send int
|
||||
@@ -52,7 +55,8 @@ func (c *Consumer) GetMedias() []*core.Media {
|
||||
func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
|
||||
trackID := byte(len(c.senders))
|
||||
|
||||
handler := core.NewSender(media, track.Codec)
|
||||
codec := track.Codec.Clone()
|
||||
handler := core.NewSender(media, codec)
|
||||
|
||||
switch track.Codec.Name {
|
||||
case core.CodecH264:
|
||||
@@ -70,10 +74,12 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv
|
||||
c.wait = waitNone
|
||||
}
|
||||
|
||||
// important to use Mutex because right fragment order
|
||||
c.mu.Lock()
|
||||
buf := c.muxer.Marshal(trackID, packet)
|
||||
c.Fire(buf)
|
||||
|
||||
c.send += len(buf)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
if track.Codec.IsRTP() {
|
||||
@@ -97,46 +103,48 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv
|
||||
c.wait = waitNone
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
buf := c.muxer.Marshal(trackID, packet)
|
||||
c.Fire(buf)
|
||||
|
||||
c.send += len(buf)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
if track.Codec.IsRTP() {
|
||||
handler.Handler = h265.RTPDepay(track.Codec, handler.Handler)
|
||||
}
|
||||
|
||||
case core.CodecAAC:
|
||||
handler.Handler = func(packet *rtp.Packet) {
|
||||
if c.wait != waitNone {
|
||||
return
|
||||
}
|
||||
|
||||
buf := c.muxer.Marshal(trackID, packet)
|
||||
c.Fire(buf)
|
||||
|
||||
c.send += len(buf)
|
||||
}
|
||||
|
||||
if track.Codec.IsRTP() {
|
||||
handler.Handler = aac.RTPDepay(handler.Handler)
|
||||
}
|
||||
|
||||
case core.CodecOpus, core.CodecMP3, core.CodecPCMU, core.CodecPCMA:
|
||||
handler.Handler = func(packet *rtp.Packet) {
|
||||
if c.wait != waitNone {
|
||||
return
|
||||
}
|
||||
|
||||
buf := c.muxer.Marshal(trackID, packet)
|
||||
c.Fire(buf)
|
||||
|
||||
c.send += len(buf)
|
||||
}
|
||||
|
||||
default:
|
||||
panic("unsupported codec")
|
||||
handler.Handler = func(packet *rtp.Packet) {
|
||||
if c.wait != waitNone {
|
||||
return
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
buf := c.muxer.Marshal(trackID, packet)
|
||||
c.Fire(buf)
|
||||
c.send += len(buf)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
switch track.Codec.Name {
|
||||
case core.CodecAAC:
|
||||
if track.Codec.IsRTP() {
|
||||
handler.Handler = aac.RTPDepay(handler.Handler)
|
||||
}
|
||||
case core.CodecOpus, core.CodecMP3: // no changes
|
||||
case core.CodecPCMA, core.CodecPCMU, core.CodecPCM:
|
||||
handler.Handler = pcm.FLACEncoder(track.Codec, handler.Handler)
|
||||
codec.Name = core.CodecFLAC
|
||||
|
||||
default:
|
||||
handler.Handler = nil
|
||||
}
|
||||
}
|
||||
|
||||
if handler.Handler == nil {
|
||||
println("ERROR: MP4 unsupported codec: " + track.Codec.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
handler.HandleRTP(track)
|
||||
|
||||
+39
-3
@@ -4,9 +4,45 @@ import "github.com/AlexxIT/go2rtc/pkg/core"
|
||||
|
||||
// ParseQuery - like usual parse, but with mp4 param handler
|
||||
func ParseQuery(query map[string][]string) []*core.Media {
|
||||
if query["mp4"] != nil {
|
||||
cons := Consumer{}
|
||||
return cons.GetMedias()
|
||||
if v := query["mp4"]; len(v) != 0 {
|
||||
medias := []*core.Media{
|
||||
{
|
||||
Kind: core.KindVideo,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecH264},
|
||||
{Name: core.CodecH265},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecAAC},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if v[0] == "" {
|
||||
return medias // legacy
|
||||
}
|
||||
|
||||
medias[1].Codecs = append(medias[1].Codecs,
|
||||
&core.Codec{Name: core.CodecPCMA},
|
||||
&core.Codec{Name: core.CodecPCMU},
|
||||
&core.Codec{Name: core.CodecPCM},
|
||||
)
|
||||
|
||||
if v[0] == "flac" {
|
||||
return medias // modern browsers
|
||||
}
|
||||
|
||||
medias[1].Codecs = append(medias[1].Codecs,
|
||||
&core.Codec{Name: core.CodecOpus},
|
||||
&core.Codec{Name: core.CodecMP3},
|
||||
)
|
||||
|
||||
return medias // Chrome, FFmpeg, VLC
|
||||
}
|
||||
|
||||
return core.ParseQuery(query)
|
||||
|
||||
+44
-18
@@ -15,12 +15,14 @@ type Muxer struct {
|
||||
fragIndex uint32
|
||||
dts []uint64
|
||||
pts []uint32
|
||||
codecs []*core.Codec
|
||||
}
|
||||
|
||||
const (
|
||||
MimeH264 = "avc1.640029"
|
||||
MimeH265 = "hvc1.1.6.L153.B0"
|
||||
MimeAAC = "mp4a.40.2"
|
||||
MimeFlac = "flac"
|
||||
MimeOpus = "opus"
|
||||
)
|
||||
|
||||
@@ -43,6 +45,8 @@ func (m *Muxer) MimeCodecs(codecs []*core.Codec) string {
|
||||
s += MimeAAC
|
||||
case core.CodecOpus:
|
||||
s += MimeOpus
|
||||
case core.CodecFLAC:
|
||||
s += MimeFlac
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,14 +112,15 @@ func (m *Muxer) GetInit(codecs []*core.Codec) ([]byte, error) {
|
||||
uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, b,
|
||||
)
|
||||
|
||||
case core.CodecOpus, core.CodecMP3, core.CodecPCMU, core.CodecPCMA:
|
||||
case core.CodecOpus, core.CodecMP3, core.CodecPCMA, core.CodecPCMU, core.CodecPCM, core.CodecFLAC:
|
||||
mv.WriteAudioTrack(
|
||||
uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, nil,
|
||||
)
|
||||
}
|
||||
|
||||
m.pts = append(m.pts, 0)
|
||||
m.dts = append(m.dts, 0)
|
||||
m.pts = append(m.pts, 0)
|
||||
m.codecs = append(m.codecs, codec)
|
||||
}
|
||||
|
||||
mv.StartAtom(iso.MoovMvex)
|
||||
@@ -138,28 +143,49 @@ func (m *Muxer) Reset() {
|
||||
}
|
||||
|
||||
func (m *Muxer) Marshal(trackID byte, packet *rtp.Packet) []byte {
|
||||
// important before increment
|
||||
time := m.dts[trackID]
|
||||
codec := m.codecs[trackID]
|
||||
|
||||
duration := packet.Timestamp - m.pts[trackID]
|
||||
m.pts[trackID] = packet.Timestamp
|
||||
|
||||
// minumum duration important for MSE in Apple Safari
|
||||
if duration == 0 || duration > codec.ClockRate {
|
||||
duration = codec.ClockRate/1000 + 1
|
||||
m.pts[trackID] += duration
|
||||
}
|
||||
|
||||
size := len(packet.Payload)
|
||||
|
||||
// flags important for Apple Finder video preview
|
||||
var flags uint32
|
||||
switch codec.Name {
|
||||
case core.CodecH264:
|
||||
if h264.IsKeyframe(packet.Payload) {
|
||||
flags = iso.SampleVideoIFrame
|
||||
} else {
|
||||
flags = iso.SampleVideoNonIFrame
|
||||
}
|
||||
case core.CodecH265:
|
||||
if h265.IsKeyframe(packet.Payload) {
|
||||
flags = iso.SampleVideoIFrame
|
||||
} else {
|
||||
flags = iso.SampleVideoNonIFrame
|
||||
}
|
||||
default:
|
||||
flags = iso.SampleAudio // not important
|
||||
}
|
||||
|
||||
m.fragIndex++
|
||||
|
||||
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
|
||||
|
||||
mv := iso.NewMovie(1024 + len(packet.Payload))
|
||||
mv := iso.NewMovie(1024 + size)
|
||||
mv.WriteMovieFragment(
|
||||
m.fragIndex, uint32(trackID+1), duration,
|
||||
uint32(len(packet.Payload)), time,
|
||||
m.fragIndex, uint32(trackID+1), duration, uint32(size), flags, m.dts[trackID],
|
||||
)
|
||||
mv.WriteData(packet.Payload)
|
||||
|
||||
//log.Printf("[MP4] track=%d ts=%6d dur=%5d idx=%3d len=%d", trackID+1, m.dts[trackID], duration, m.fragIndex, len(packet.Payload))
|
||||
|
||||
m.dts[trackID] += uint64(duration)
|
||||
|
||||
return mv.Bytes()
|
||||
}
|
||||
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
// Package pcm - support raw (verbatim) PCM 16 bit in the FLAC container:
|
||||
// - only 1 channel
|
||||
// - only 16 bit per sample
|
||||
// - only 8000, 16000, 24000, 48000 sample rate
|
||||
package pcm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtp"
|
||||
"github.com/sigurn/crc16"
|
||||
"github.com/sigurn/crc8"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func FLACHeader(magic bool, sampleRate uint32) []byte {
|
||||
b := make([]byte, 42)
|
||||
|
||||
if magic {
|
||||
copy(b, "fLaC") // [0..3]
|
||||
}
|
||||
|
||||
// https://xiph.org/flac/format.html#metadata_block_header
|
||||
b[4] = 0x80 // [4] lastMetadata=1 (1 bit), blockType=0 - STREAMINFO (7 bit)
|
||||
b[7] = 0x22 // [5..7] blockLength=34 (24 bit)
|
||||
|
||||
// Important for Apple QuickTime player:
|
||||
// 1. Both values should be same
|
||||
// 2. Maximum value = 32768
|
||||
binary.BigEndian.PutUint16(b[8:], 32768) // [8..9] info.BlockSizeMin=16 (16 bit)
|
||||
binary.BigEndian.PutUint16(b[10:], 32768) // [10..11] info.BlockSizeMin=65535 (16 bit)
|
||||
|
||||
// [12..14] info.FrameSizeMin=0 (24 bit)
|
||||
// [15..17] info.FrameSizeMax=0 (24 bit)
|
||||
|
||||
b[18] = byte(sampleRate >> 12)
|
||||
b[19] = byte(sampleRate >> 4)
|
||||
b[20] = byte(sampleRate << 4) // [18..20] info.SampleRate=8000 (20 bit), info.NChannels=1-1 (3 bit)
|
||||
|
||||
b[21] = 0xF0 // [21..25] info.BitsPerSample=16-1 (5 bit), info.NSamples (36 bit)
|
||||
|
||||
// [26..41] MD5sum (16 bytes)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
var table8 *crc8.Table
|
||||
var table16 *crc16.Table
|
||||
|
||||
func FLACEncoder(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
|
||||
if codec.Channels >= 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var sr byte
|
||||
switch codec.ClockRate {
|
||||
case 8000:
|
||||
sr = 0b0100
|
||||
case 16000:
|
||||
sr = 0b0101
|
||||
case 22050:
|
||||
sr = 0b0110
|
||||
case 24000:
|
||||
sr = 0b0111
|
||||
case 32000:
|
||||
sr = 0b1000
|
||||
case 44100:
|
||||
sr = 0b1001
|
||||
case 48000:
|
||||
sr = 0b1010
|
||||
case 96000:
|
||||
sr = 0b1011
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
if table8 == nil {
|
||||
table8 = crc8.MakeTable(crc8.CRC8)
|
||||
}
|
||||
if table16 == nil {
|
||||
table16 = crc16.MakeTable(crc16.CRC16_BUYPASS)
|
||||
}
|
||||
|
||||
var sampleNumber int32
|
||||
|
||||
return func(packet *rtp.Packet) {
|
||||
samples := uint16(len(packet.Payload))
|
||||
|
||||
if codec.Name == core.CodecPCM {
|
||||
samples /= 2
|
||||
}
|
||||
|
||||
// https://xiph.org/flac/format.html#frame_header
|
||||
buf := make([]byte, samples*2+30)
|
||||
|
||||
// 1. Frame header
|
||||
buf[0] = 0xFF
|
||||
buf[1] = 0xF9 // [0..1] syncCode=0xFFF8 - reserved (15 bit), blockStrategy=1 - variable-blocksize (1 bit)
|
||||
buf[2] = 0x70 | sr // blockSizeType=7 (4 bit), sampleRate=4 - 8000 (4 bit)
|
||||
buf[3] = 0x08 // channels=1-1 (4 bit), sampleSize=4 - 16 (3 bit), reserved=0 (1 bit)
|
||||
|
||||
n := 4 + utf8.EncodeRune(buf[4:], sampleNumber) // 4 bytes max
|
||||
sampleNumber += int32(samples)
|
||||
|
||||
// this is wrong but very simple frame block size value
|
||||
binary.BigEndian.PutUint16(buf[n:], samples-1)
|
||||
n += 2
|
||||
|
||||
buf[n] = crc8.Checksum(buf[:n], table8)
|
||||
n += 1
|
||||
|
||||
// 2. Subframe header
|
||||
buf[n] = 0x02 // padding=0 (1 bit), subframeType=1 - verbatim (6 bit), wastedFlag=0 (1 bit)
|
||||
n += 1
|
||||
|
||||
// 3. Subframe
|
||||
switch codec.Name {
|
||||
case core.CodecPCMA:
|
||||
for _, b := range packet.Payload {
|
||||
s16 := PCMAtoPCM(b)
|
||||
buf[n] = byte(s16 >> 8)
|
||||
buf[n+1] = byte(s16)
|
||||
n += 2
|
||||
}
|
||||
case core.CodecPCMU:
|
||||
for _, b := range packet.Payload {
|
||||
s16 := PCMUtoPCM(b)
|
||||
buf[n] = byte(s16 >> 8)
|
||||
buf[n+1] = byte(s16)
|
||||
n += 2
|
||||
}
|
||||
case core.CodecPCM:
|
||||
n += copy(buf[n:], packet.Payload)
|
||||
}
|
||||
|
||||
// 4. Frame footer
|
||||
crc := crc16.Checksum(buf[:n], table16)
|
||||
binary.BigEndian.PutUint16(buf[n:], crc)
|
||||
n += 2
|
||||
|
||||
clone := *packet
|
||||
clone.Payload = buf[:n]
|
||||
|
||||
handler(&clone)
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package pcm
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
func Resample(codec *core.Codec, sampleRate uint32, handler core.HandlerFunc) core.HandlerFunc {
|
||||
n := float32(codec.ClockRate) / float32(sampleRate)
|
||||
|
||||
switch codec.Name {
|
||||
case core.CodecPCMA:
|
||||
return DownsampleByte(PCMAtoPCM, PCMtoPCMA, n, handler)
|
||||
case core.CodecPCMU:
|
||||
return DownsampleByte(PCMUtoPCM, PCMtoPCMU, n, handler)
|
||||
case core.CodecPCM:
|
||||
if n == 1 {
|
||||
return ResamplePCM(PCMtoPCMA, handler)
|
||||
}
|
||||
return DownsamplePCM(PCMtoPCMA, n, handler)
|
||||
}
|
||||
|
||||
panic(core.Caller())
|
||||
}
|
||||
|
||||
func DownsampleByte(
|
||||
toPCM func(byte) int16, fromPCM func(int16) byte, n float32, handler core.HandlerFunc,
|
||||
) core.HandlerFunc {
|
||||
var sampleN, sampleSum float32
|
||||
var ts uint32
|
||||
|
||||
return func(packet *rtp.Packet) {
|
||||
samples := len(packet.Payload)
|
||||
newLen := uint32((float32(samples) + sampleN) / n)
|
||||
|
||||
oldSamples := packet.Payload
|
||||
newSamples := make([]byte, newLen)
|
||||
|
||||
var i int
|
||||
for _, sample := range oldSamples {
|
||||
sampleSum += float32(toPCM(sample))
|
||||
if sampleN++; sampleN >= n {
|
||||
newSamples[i] = fromPCM(int16(sampleSum / n))
|
||||
i++
|
||||
|
||||
sampleSum = 0
|
||||
sampleN -= n
|
||||
}
|
||||
}
|
||||
|
||||
ts += newLen
|
||||
|
||||
clone := *packet
|
||||
clone.Payload = newSamples
|
||||
clone.Timestamp = ts
|
||||
handler(&clone)
|
||||
}
|
||||
}
|
||||
|
||||
func ResamplePCM(fromPCM func(int16) byte, handler core.HandlerFunc) core.HandlerFunc {
|
||||
var ts uint32
|
||||
|
||||
return func(packet *rtp.Packet) {
|
||||
len1 := len(packet.Payload)
|
||||
len2 := len1 / 2
|
||||
|
||||
oldSamples := packet.Payload
|
||||
newSamples := make([]byte, len2)
|
||||
|
||||
var i2 int
|
||||
for i1 := 0; i1 < len1; i1 += 2 {
|
||||
sample := int16(uint16(oldSamples[i1])<<8 | uint16(oldSamples[i1+1]))
|
||||
newSamples[i2] = fromPCM(sample)
|
||||
i2++
|
||||
}
|
||||
|
||||
ts += uint32(len2)
|
||||
|
||||
clone := *packet
|
||||
clone.Payload = newSamples
|
||||
clone.Timestamp = ts
|
||||
handler(&clone)
|
||||
}
|
||||
}
|
||||
|
||||
func DownsamplePCM(fromPCM func(int16) byte, n float32, handler core.HandlerFunc) core.HandlerFunc {
|
||||
var sampleN, sampleSum float32
|
||||
var ts uint32
|
||||
|
||||
return func(packet *rtp.Packet) {
|
||||
samples := len(packet.Payload) / 2
|
||||
newLen := uint32((float32(samples) + sampleN) / n)
|
||||
|
||||
oldSamples := packet.Payload
|
||||
newSamples := make([]byte, newLen)
|
||||
|
||||
var i2 int
|
||||
for i1 := 0; i1 < len(packet.Payload); i1 += 2 {
|
||||
sampleSum += float32(int16(uint16(oldSamples[i1])<<8 | uint16(oldSamples[i1+1])))
|
||||
if sampleN++; sampleN >= n {
|
||||
newSamples[i2] = fromPCM(int16(sampleSum / n))
|
||||
i2++
|
||||
|
||||
sampleSum = 0
|
||||
sampleN -= n
|
||||
}
|
||||
}
|
||||
|
||||
ts += newLen
|
||||
|
||||
clone := *packet
|
||||
clone.Payload = newSamples
|
||||
clone.Timestamp = ts
|
||||
handler(&clone)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Package pcm
|
||||
// https://www.codeproject.com/Articles/14237/Using-the-G711-standard
|
||||
package pcm
|
||||
|
||||
const alawMax = 0x7FFF
|
||||
|
||||
func PCMAtoPCM(alaw byte) int16 {
|
||||
alaw ^= 0xD5
|
||||
|
||||
data := int16(((alaw & 0x0F) << 4) + 8)
|
||||
exponent := (alaw & 0x70) >> 4
|
||||
|
||||
if exponent != 0 {
|
||||
data |= 0x100
|
||||
}
|
||||
|
||||
if exponent > 1 {
|
||||
data <<= exponent - 1
|
||||
}
|
||||
|
||||
// sign
|
||||
if alaw&0x80 == 0 {
|
||||
return data
|
||||
} else {
|
||||
return -data
|
||||
}
|
||||
}
|
||||
|
||||
func PCMtoPCMA(pcm int16) byte {
|
||||
var alaw byte
|
||||
|
||||
if pcm < 0 {
|
||||
pcm = -pcm
|
||||
alaw = 0x80
|
||||
}
|
||||
|
||||
if pcm > alawMax {
|
||||
pcm = alawMax
|
||||
}
|
||||
|
||||
exponent := byte(7)
|
||||
for expMask := int16(0x4000); (pcm&expMask) == 0 && exponent > 0; expMask >>= 1 {
|
||||
exponent--
|
||||
}
|
||||
|
||||
if exponent == 0 {
|
||||
alaw |= byte(pcm>>4) & 0x0F
|
||||
} else {
|
||||
alaw |= (exponent << 4) | (byte(pcm>>(exponent+3)) & 0x0F)
|
||||
}
|
||||
|
||||
return alaw ^ 0xD5
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Package pcm
|
||||
// https://www.codeproject.com/Articles/14237/Using-the-G711-standard
|
||||
package pcm
|
||||
|
||||
const bias = 0x84 // 132 or 1000 0100
|
||||
const ulawMax = alawMax - bias
|
||||
|
||||
func PCMUtoPCM(ulaw byte) int16 {
|
||||
ulaw = ^ulaw
|
||||
|
||||
exponent := (ulaw & 0x70) >> 4
|
||||
data := (int16((((ulaw&0x0F)|0x10)<<1)+1) << (exponent + 2)) - bias
|
||||
|
||||
// sign
|
||||
if ulaw&0x80 == 0 {
|
||||
return data
|
||||
} else if data == 0 {
|
||||
return -1
|
||||
} else {
|
||||
return -data
|
||||
}
|
||||
}
|
||||
|
||||
func PCMtoPCMU(pcm int16) byte {
|
||||
var ulaw byte
|
||||
|
||||
if pcm < 0 {
|
||||
pcm = -pcm
|
||||
ulaw = 0x80
|
||||
}
|
||||
|
||||
if pcm > ulawMax {
|
||||
pcm = ulawMax
|
||||
}
|
||||
|
||||
pcm += bias
|
||||
|
||||
exponent := byte(7)
|
||||
for expMask := int16(0x4000); (pcm & expMask) == 0; expMask >>= 1 {
|
||||
exponent--
|
||||
}
|
||||
|
||||
// mantisa
|
||||
ulaw |= byte(pcm>>(exponent+3)) & 0x0F
|
||||
|
||||
if exponent > 0 {
|
||||
ulaw |= exponent << 4
|
||||
}
|
||||
|
||||
return ^ulaw
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// Package v1
|
||||
// http://web.archive.org/web/20110719132013/http://hazelware.luggle.com/tutorials/mulawcompression.html
|
||||
package v1
|
||||
|
||||
const cBias = 0x84
|
||||
const cClip = 32635
|
||||
|
||||
var MuLawCompressTable = [256]byte{
|
||||
0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3,
|
||||
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
|
||||
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
|
||||
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
|
||||
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
|
||||
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
|
||||
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
|
||||
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
||||
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
||||
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
||||
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
||||
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
||||
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
||||
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
||||
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
||||
}
|
||||
|
||||
func LinearToMuLawSample(sample int16) byte {
|
||||
sign := byte(sample>>8) & 0x80
|
||||
if sign != 0 {
|
||||
sample = -sample
|
||||
}
|
||||
|
||||
if sample > cClip {
|
||||
sample = cClip
|
||||
}
|
||||
sample = sample + cBias
|
||||
|
||||
exponent := MuLawCompressTable[(sample>>7)&0xFF]
|
||||
mantissa := byte(sample>>(exponent+3)) & 0x0F
|
||||
|
||||
compressedByte := ^(sign | (exponent << 4) | mantissa)
|
||||
|
||||
return compressedByte
|
||||
}
|
||||
|
||||
var ALawCompressTable = [128]byte{
|
||||
1, 1, 2, 2, 3, 3, 3, 3,
|
||||
4, 4, 4, 4, 4, 4, 4, 4,
|
||||
5, 5, 5, 5, 5, 5, 5, 5,
|
||||
5, 5, 5, 5, 5, 5, 5, 5,
|
||||
6, 6, 6, 6, 6, 6, 6, 6,
|
||||
6, 6, 6, 6, 6, 6, 6, 6,
|
||||
6, 6, 6, 6, 6, 6, 6, 6,
|
||||
6, 6, 6, 6, 6, 6, 6, 6,
|
||||
7, 7, 7, 7, 7, 7, 7, 7,
|
||||
7, 7, 7, 7, 7, 7, 7, 7,
|
||||
7, 7, 7, 7, 7, 7, 7, 7,
|
||||
7, 7, 7, 7, 7, 7, 7, 7,
|
||||
7, 7, 7, 7, 7, 7, 7, 7,
|
||||
7, 7, 7, 7, 7, 7, 7, 7,
|
||||
7, 7, 7, 7, 7, 7, 7, 7,
|
||||
7, 7, 7, 7, 7, 7, 7, 7,
|
||||
}
|
||||
|
||||
func LinearToALawSample(sample int16) byte {
|
||||
sign := byte((^sample)>>8) & 0x80
|
||||
if sign == 0 {
|
||||
sample = -sample
|
||||
}
|
||||
|
||||
if sample > cClip {
|
||||
sample = cClip
|
||||
}
|
||||
|
||||
var compressedByte byte
|
||||
if sample >= 256 {
|
||||
exponent := ALawCompressTable[(sample>>8)&0x7F]
|
||||
mantissa := byte(sample>>(exponent+3)) & 0x0F
|
||||
compressedByte = (exponent << 4) | mantissa
|
||||
} else {
|
||||
compressedByte = byte(sample >> 4)
|
||||
}
|
||||
compressedByte ^= sign ^ 0x55
|
||||
return compressedByte
|
||||
}
|
||||
|
||||
var MuLawDecompressTable = [256]int16{
|
||||
-32124, -31100, -30076, -29052, -28028, -27004, -25980, -24956,
|
||||
-23932, -22908, -21884, -20860, -19836, -18812, -17788, -16764,
|
||||
-15996, -15484, -14972, -14460, -13948, -13436, -12924, -12412,
|
||||
-11900, -11388, -10876, -10364, -9852, -9340, -8828, -8316,
|
||||
-7932, -7676, -7420, -7164, -6908, -6652, -6396, -6140,
|
||||
-5884, -5628, -5372, -5116, -4860, -4604, -4348, -4092,
|
||||
-3900, -3772, -3644, -3516, -3388, -3260, -3132, -3004,
|
||||
-2876, -2748, -2620, -2492, -2364, -2236, -2108, -1980,
|
||||
-1884, -1820, -1756, -1692, -1628, -1564, -1500, -1436,
|
||||
-1372, -1308, -1244, -1180, -1116, -1052, -988, -924,
|
||||
-876, -844, -812, -780, -748, -716, -684, -652,
|
||||
-620, -588, -556, -524, -492, -460, -428, -396,
|
||||
-372, -356, -340, -324, -308, -292, -276, -260,
|
||||
-244, -228, -212, -196, -180, -164, -148, -132,
|
||||
-120, -112, -104, -96, -88, -80, -72, -64,
|
||||
-56, -48, -40, -32, -24, -16, -8, -1,
|
||||
32124, 31100, 30076, 29052, 28028, 27004, 25980, 24956,
|
||||
23932, 22908, 21884, 20860, 19836, 18812, 17788, 16764,
|
||||
15996, 15484, 14972, 14460, 13948, 13436, 12924, 12412,
|
||||
11900, 11388, 10876, 10364, 9852, 9340, 8828, 8316,
|
||||
7932, 7676, 7420, 7164, 6908, 6652, 6396, 6140,
|
||||
5884, 5628, 5372, 5116, 4860, 4604, 4348, 4092,
|
||||
3900, 3772, 3644, 3516, 3388, 3260, 3132, 3004,
|
||||
2876, 2748, 2620, 2492, 2364, 2236, 2108, 1980,
|
||||
1884, 1820, 1756, 1692, 1628, 1564, 1500, 1436,
|
||||
1372, 1308, 1244, 1180, 1116, 1052, 988, 924,
|
||||
876, 844, 812, 780, 748, 716, 684, 652,
|
||||
620, 588, 556, 524, 492, 460, 428, 396,
|
||||
372, 356, 340, 324, 308, 292, 276, 260,
|
||||
244, 228, 212, 196, 180, 164, 148, 132,
|
||||
120, 112, 104, 96, 88, 80, 72, 64,
|
||||
56, 48, 40, 32, 24, 16, 8, 0,
|
||||
}
|
||||
|
||||
var ALawDecompressTable = [256]int16{
|
||||
-5504, -5248, -6016, -5760, -4480, -4224, -4992, -4736,
|
||||
-7552, -7296, -8064, -7808, -6528, -6272, -7040, -6784,
|
||||
-2752, -2624, -3008, -2880, -2240, -2112, -2496, -2368,
|
||||
-3776, -3648, -4032, -3904, -3264, -3136, -3520, -3392,
|
||||
-22016, -20992, -24064, -23040, -17920, -16896, -19968, -18944,
|
||||
-30208, -29184, -32256, -31232, -26112, -25088, -28160, -27136,
|
||||
-11008, -10496, -12032, -11520, -8960, -8448, -9984, -9472,
|
||||
-15104, -14592, -16128, -15616, -13056, -12544, -14080, -13568,
|
||||
-344, -328, -376, -360, -280, -264, -312, -296,
|
||||
-472, -456, -504, -488, -408, -392, -440, -424,
|
||||
-88, -72, -120, -104, -24, -8, -56, -40,
|
||||
-216, -200, -248, -232, -152, -136, -184, -168,
|
||||
-1376, -1312, -1504, -1440, -1120, -1056, -1248, -1184,
|
||||
-1888, -1824, -2016, -1952, -1632, -1568, -1760, -1696,
|
||||
-688, -656, -752, -720, -560, -528, -624, -592,
|
||||
-944, -912, -1008, -976, -816, -784, -880, -848,
|
||||
5504, 5248, 6016, 5760, 4480, 4224, 4992, 4736,
|
||||
7552, 7296, 8064, 7808, 6528, 6272, 7040, 6784,
|
||||
2752, 2624, 3008, 2880, 2240, 2112, 2496, 2368,
|
||||
3776, 3648, 4032, 3904, 3264, 3136, 3520, 3392,
|
||||
22016, 20992, 24064, 23040, 17920, 16896, 19968, 18944,
|
||||
30208, 29184, 32256, 31232, 26112, 25088, 28160, 27136,
|
||||
11008, 10496, 12032, 11520, 8960, 8448, 9984, 9472,
|
||||
15104, 14592, 16128, 15616, 13056, 12544, 14080, 13568,
|
||||
344, 328, 376, 360, 280, 264, 312, 296,
|
||||
472, 456, 504, 488, 408, 392, 440, 424,
|
||||
88, 72, 120, 104, 24, 8, 56, 40,
|
||||
216, 200, 248, 232, 152, 136, 184, 168,
|
||||
1376, 1312, 1504, 1440, 1120, 1056, 1248, 1184,
|
||||
1888, 1824, 2016, 1952, 1632, 1568, 1760, 1696,
|
||||
688, 656, 752, 720, 560, 528, 624, 592,
|
||||
944, 912, 1008, 976, 816, 784, 880, 848,
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
v2 "github.com/AlexxIT/go2rtc/pkg/pcm"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPCMUtoPCM(t *testing.T) {
|
||||
for pcmu := byte(0); pcmu < 255; pcmu++ {
|
||||
pcm1 := MuLawDecompressTable[pcmu]
|
||||
pcm2 := v2.PCMUtoPCM(pcmu)
|
||||
require.Equal(t, pcm1, pcm2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPCMAtoPCM(t *testing.T) {
|
||||
for pcma := byte(0); pcma < 255; pcma++ {
|
||||
pcm1 := ALawDecompressTable[pcma]
|
||||
pcm2 := v2.PCMAtoPCM(pcma)
|
||||
require.Equal(t, pcm1, pcm2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPCMtoPCMU(t *testing.T) {
|
||||
for pcm := int16(-32768); pcm < 32767; pcm++ {
|
||||
pcmu1 := LinearToMuLawSample(pcm)
|
||||
pcmu2 := v2.PCMtoPCMU(pcm)
|
||||
require.Equal(t, pcmu1, pcmu2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPCMtoPCMA(t *testing.T) {
|
||||
for pcm := int16(-32768); pcm < 32767; pcm++ {
|
||||
pcma1 := LinearToALawSample(pcm)
|
||||
pcma2 := v2.PCMtoPCMA(pcm)
|
||||
require.Equal(t, pcma1, pcma2)
|
||||
}
|
||||
}
|
||||
+34
-137
@@ -5,16 +5,19 @@ import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
)
|
||||
|
||||
var Timeout = time.Second * 5
|
||||
|
||||
func NewClient(uri string) *Conn {
|
||||
return &Conn{uri: uri}
|
||||
}
|
||||
@@ -28,10 +31,6 @@ func (c *Conn) Dial() (err error) {
|
||||
c.URL.Host += ":554"
|
||||
}
|
||||
|
||||
// remove UserInfo from URL
|
||||
c.auth = tcp.NewAuth(c.URL.User)
|
||||
c.URL.User = nil
|
||||
|
||||
c.conn, err = net.DialTimeout("tcp", c.URL.Host, time.Second*5)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -53,50 +52,24 @@ func (c *Conn) Dial() (err error) {
|
||||
c.conn = tlsConn
|
||||
}
|
||||
|
||||
// remove UserInfo from URL
|
||||
c.auth = tcp.NewAuth(c.URL.User)
|
||||
c.URL.User = nil
|
||||
|
||||
c.reader = bufio.NewReader(c.conn)
|
||||
c.session = ""
|
||||
c.state = StateConn
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Request sends only Request
|
||||
func (c *Conn) Request(req *tcp.Request) error {
|
||||
if req.Proto == "" {
|
||||
req.Proto = ProtoRTSP
|
||||
}
|
||||
|
||||
if req.Header == nil {
|
||||
req.Header = make(map[string][]string)
|
||||
}
|
||||
|
||||
c.sequence++
|
||||
// important to send case sensitive CSeq
|
||||
// https://github.com/AlexxIT/go2rtc/issues/7
|
||||
req.Header["CSeq"] = []string{strconv.Itoa(c.sequence)}
|
||||
|
||||
c.auth.Write(req)
|
||||
|
||||
if c.Session != "" {
|
||||
req.Header.Set("Session", c.Session)
|
||||
}
|
||||
|
||||
if req.Body != nil {
|
||||
val := strconv.Itoa(len(req.Body))
|
||||
req.Header.Set("Content-Length", val)
|
||||
}
|
||||
|
||||
c.Fire(req)
|
||||
|
||||
return req.Write(c.conn)
|
||||
}
|
||||
|
||||
// Do send Request and receive and process Response
|
||||
// Do send WriteRequest and receive and process WriteResponse
|
||||
func (c *Conn) Do(req *tcp.Request) (*tcp.Response, error) {
|
||||
if err := c.Request(req); err != nil {
|
||||
if err := c.WriteRequest(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := tcp.ReadResponse(c.reader)
|
||||
res, err := c.ReadResponse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -126,40 +99,6 @@ func (c *Conn) Do(req *tcp.Request) (*tcp.Response, error) {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *Conn) Response(res *tcp.Response) error {
|
||||
if res.Proto == "" {
|
||||
res.Proto = ProtoRTSP
|
||||
}
|
||||
|
||||
if res.Status == "" {
|
||||
res.Status = "200 OK"
|
||||
}
|
||||
|
||||
if res.Header == nil {
|
||||
res.Header = make(map[string][]string)
|
||||
}
|
||||
|
||||
if res.Request != nil && res.Request.Header != nil {
|
||||
seq := res.Request.Header.Get("CSeq")
|
||||
if seq != "" {
|
||||
res.Header.Set("CSeq", seq)
|
||||
}
|
||||
}
|
||||
|
||||
if c.Session != "" {
|
||||
res.Header.Set("Session", c.Session)
|
||||
}
|
||||
|
||||
if res.Body != nil {
|
||||
val := strconv.Itoa(len(res.Body))
|
||||
res.Header.Set("Content-Length", val)
|
||||
}
|
||||
|
||||
c.Fire(res)
|
||||
|
||||
return res.Write(c.conn)
|
||||
}
|
||||
|
||||
func (c *Conn) Options() error {
|
||||
req := &tcp.Request{Method: MethodOptions, URL: c.URL}
|
||||
|
||||
@@ -211,11 +150,18 @@ func (c *Conn) Describe() error {
|
||||
}
|
||||
}
|
||||
|
||||
c.Medias, err = UnmarshalSDP(res.Body)
|
||||
medias, err := UnmarshalSDP(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: rewrite more smart
|
||||
if c.Medias == nil {
|
||||
c.Medias = medias
|
||||
} else if len(c.Medias) > len(medias) {
|
||||
c.Medias = c.Medias[:len(medias)]
|
||||
}
|
||||
|
||||
c.mode = core.ModeActiveProducer
|
||||
|
||||
return nil
|
||||
@@ -242,33 +188,12 @@ func (c *Conn) Announce() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Conn) Setup() error {
|
||||
for _, media := range c.Medias {
|
||||
_, err := c.SetupMedia(media, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) SetupMedia(media *core.Media, first bool) (byte, error) {
|
||||
// TODO: rewrite recoonection and first flag
|
||||
if first {
|
||||
c.stateMu.Lock()
|
||||
defer c.stateMu.Unlock()
|
||||
}
|
||||
|
||||
if c.state != StateConn && c.state != StateSetup {
|
||||
return 0, fmt.Errorf("RTSP SETUP from wrong state: %s", c.state)
|
||||
}
|
||||
|
||||
func (c *Conn) SetupMedia(media *core.Media) (byte, error) {
|
||||
var transport string
|
||||
|
||||
// try to use media position as channel number
|
||||
for i, m := range c.Medias {
|
||||
if m.ID == media.ID {
|
||||
if m.Equal(media) {
|
||||
transport = fmt.Sprintf(
|
||||
// i - RTP (data channel)
|
||||
// i+1 - RTCP (control channel)
|
||||
@@ -303,37 +228,28 @@ func (c *Conn) SetupMedia(media *core.Media, first bool) (byte, error) {
|
||||
},
|
||||
}
|
||||
|
||||
var res *tcp.Response
|
||||
res, err = c.Do(req)
|
||||
res, err := c.Do(req)
|
||||
if err != nil {
|
||||
// some Dahua/Amcrest cameras fail here because two simultaneous
|
||||
// backchannel connections
|
||||
if c.Backchannel {
|
||||
c.Backchannel = false
|
||||
if err := c.Dial(); err != nil {
|
||||
if err = c.Reconnect(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := c.Describe(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
for _, newMedia := range c.Medias {
|
||||
if newMedia.ID == media.ID {
|
||||
return c.SetupMedia(newMedia, false)
|
||||
}
|
||||
}
|
||||
return c.SetupMedia(media)
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if c.Session == "" {
|
||||
if c.session == "" {
|
||||
// Session: 216525287999;timeout=60
|
||||
if s := res.Header.Get("Session"); s != "" {
|
||||
if j := strings.IndexByte(s, ';'); j > 0 {
|
||||
s = s[:j]
|
||||
c.session, s, _ = strings.Cut(s, ";timeout=")
|
||||
if s != "" {
|
||||
c.keepalive, _ = strconv.Atoi(s)
|
||||
}
|
||||
c.Session = s
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,8 +267,6 @@ func (c *Conn) SetupMedia(media *core.Media, first bool) (byte, error) {
|
||||
}
|
||||
}
|
||||
|
||||
c.state = StateSetup
|
||||
|
||||
channel := core.Between(transport, "interleaved=", "-")
|
||||
i, err := strconv.Atoi(channel)
|
||||
if err != nil {
|
||||
@@ -363,36 +277,19 @@ func (c *Conn) SetupMedia(media *core.Media, first bool) (byte, error) {
|
||||
}
|
||||
|
||||
func (c *Conn) Play() (err error) {
|
||||
c.stateMu.Lock()
|
||||
defer c.stateMu.Unlock()
|
||||
|
||||
if c.state != StateSetup {
|
||||
return fmt.Errorf("RTSP PLAY from wrong state: %s", c.state)
|
||||
}
|
||||
|
||||
req := &tcp.Request{Method: MethodPlay, URL: c.URL}
|
||||
if err = c.Request(req); err == nil {
|
||||
c.state = StatePlay
|
||||
}
|
||||
|
||||
return
|
||||
return c.WriteRequest(req)
|
||||
}
|
||||
|
||||
func (c *Conn) Teardown() (err error) {
|
||||
// allow TEARDOWN from any state (ex. ANNOUNCE > SETUP)
|
||||
req := &tcp.Request{Method: MethodTeardown, URL: c.URL}
|
||||
return c.Request(req)
|
||||
return c.WriteRequest(req)
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
c.stateMu.Lock()
|
||||
defer c.stateMu.Unlock()
|
||||
|
||||
if c.state == StateNone {
|
||||
return nil
|
||||
if c.mode == core.ModeActiveProducer {
|
||||
_ = c.Teardown()
|
||||
}
|
||||
|
||||
_ = c.Teardown()
|
||||
c.state = StateNone
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package rtsp
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTimeout(t *testing.T) {
|
||||
Timeout = time.Millisecond
|
||||
|
||||
ln, err := net.Listen("tcp", "localhost:0")
|
||||
require.Nil(t, err)
|
||||
|
||||
client := NewClient("rtsp://" + ln.Addr().String() + "/stream")
|
||||
client.Backchannel = true
|
||||
|
||||
err = client.Dial()
|
||||
require.Nil(t, err)
|
||||
|
||||
err = client.Describe()
|
||||
require.ErrorIs(t, err, os.ErrDeadlineExceeded)
|
||||
}
|
||||
|
||||
func TestMissedControl(t *testing.T) {
|
||||
Timeout = time.Millisecond
|
||||
|
||||
ln, err := net.Listen("tcp", "localhost:0")
|
||||
require.Nil(t, err)
|
||||
|
||||
go func() {
|
||||
conn, err := ln.Accept()
|
||||
require.Nil(t, err)
|
||||
|
||||
b := make([]byte, 8192)
|
||||
for {
|
||||
n, err := conn.Read(b)
|
||||
require.Nil(t, err)
|
||||
|
||||
req := string(b[:n])
|
||||
|
||||
switch req[:4] {
|
||||
case "DESC":
|
||||
_, _ = conn.Write([]byte(`RTSP/1.0 200 OK
|
||||
Cseq: 1
|
||||
Content-Length: 495
|
||||
Content-Type: application/sdp
|
||||
|
||||
v=0
|
||||
o=- 1 1 IN IP4 0.0.0.0
|
||||
s=go2rtc/1.2.0
|
||||
c=IN IP4 0.0.0.0
|
||||
t=0 0
|
||||
m=audio 0 RTP/AVP 96
|
||||
a=rtpmap:96 MPEG4-GENERIC/48000/2
|
||||
a=fmtp:96 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3; config=119056E500
|
||||
m=audio 0 RTP/AVP 97
|
||||
a=rtpmap:97 OPUS/48000/2
|
||||
a=fmtp:97 sprop-stereo=1
|
||||
m=video 0 RTP/AVP 98
|
||||
a=rtpmap:98 H264/90000
|
||||
a=fmtp:98 packetization-mode=1; sprop-parameter-sets=Z2QAKaw0yAeAIn5cBagICAoAAAfQAAE4gdDAAjhAACOEF3lxoYAEcIAARwgu8uFA,aO48MAA=; profile-level-id=640029
|
||||
`))
|
||||
|
||||
case "SETU":
|
||||
_, _ = conn.Write([]byte(`RTSP/1.0 200 OK
|
||||
Transport: RTP/AVP/TCP;unicast;interleaved=4-5
|
||||
Cseq: 3
|
||||
Session: 1
|
||||
|
||||
`))
|
||||
|
||||
default:
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
client := NewClient("rtsp://" + ln.Addr().String() + "/stream")
|
||||
client.Backchannel = true
|
||||
|
||||
err = client.Dial()
|
||||
require.Nil(t, err)
|
||||
|
||||
err = client.Describe()
|
||||
require.Nil(t, err)
|
||||
require.Len(t, client.Medias, 3)
|
||||
|
||||
ch, err := client.SetupMedia(client.Medias[2], true)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, ch, byte(4))
|
||||
}
|
||||
+126
-52
@@ -25,20 +25,22 @@ type Conn struct {
|
||||
SessionName string
|
||||
|
||||
Medias []*core.Media
|
||||
Session string
|
||||
UserAgent string
|
||||
URL *url.URL
|
||||
|
||||
// internal
|
||||
|
||||
auth *tcp.Auth
|
||||
conn net.Conn
|
||||
mode core.Mode
|
||||
state State
|
||||
stateMu sync.Mutex
|
||||
reader *bufio.Reader
|
||||
sequence int
|
||||
uri string
|
||||
auth *tcp.Auth
|
||||
conn net.Conn
|
||||
keepalive int
|
||||
mode core.Mode
|
||||
reader *bufio.Reader
|
||||
sequence int
|
||||
session string
|
||||
uri string
|
||||
|
||||
state State
|
||||
stateMu sync.Mutex
|
||||
|
||||
receivers []*core.Receiver
|
||||
senders []*core.Sender
|
||||
@@ -68,13 +70,12 @@ func (s State) String() string {
|
||||
case StateNone:
|
||||
return "NONE"
|
||||
case StateConn:
|
||||
|
||||
return "CONN"
|
||||
case StateSetup:
|
||||
return "SETUP"
|
||||
return MethodSetup
|
||||
case StatePlay:
|
||||
return "PLAY"
|
||||
case StateHandle:
|
||||
return "HANDLE"
|
||||
return MethodPlay
|
||||
}
|
||||
return strconv.Itoa(int(s))
|
||||
}
|
||||
@@ -84,38 +85,24 @@ const (
|
||||
StateConn
|
||||
StateSetup
|
||||
StatePlay
|
||||
StateHandle
|
||||
)
|
||||
|
||||
func (c *Conn) Handle() (err error) {
|
||||
c.stateMu.Lock()
|
||||
|
||||
switch c.state {
|
||||
case StateNone: // Close after PLAY and before Handle is OK (because SETUP after PLAY)
|
||||
case StatePlay:
|
||||
c.state = StateHandle
|
||||
default:
|
||||
err = fmt.Errorf("RTSP HANDLE from wrong state: %s", c.state)
|
||||
|
||||
c.state = StateNone
|
||||
_ = c.conn.Close()
|
||||
}
|
||||
|
||||
ok := c.state == StateHandle
|
||||
|
||||
c.stateMu.Unlock()
|
||||
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var timeout time.Duration
|
||||
|
||||
var keepaliveDT time.Duration
|
||||
var keepaliveTS time.Time
|
||||
|
||||
switch c.mode {
|
||||
case core.ModeActiveProducer:
|
||||
// polling frames from remote RTSP Server (ex Camera)
|
||||
go c.keepalive()
|
||||
if c.keepalive > 5 {
|
||||
keepaliveDT = time.Duration(c.keepalive-5) * time.Second
|
||||
} else {
|
||||
keepaliveDT = 25 * time.Second
|
||||
}
|
||||
keepaliveTS = time.Now().Add(keepaliveDT)
|
||||
|
||||
// polling frames from remote RTSP Server (ex Camera)
|
||||
if len(c.receivers) > 0 {
|
||||
// if we receiving video/audio from camera
|
||||
timeout = time.Second * 5
|
||||
@@ -137,7 +124,9 @@ func (c *Conn) Handle() (err error) {
|
||||
}
|
||||
|
||||
for c.state != StateNone {
|
||||
if err = c.conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
|
||||
ts := time.Now()
|
||||
|
||||
if err = c.conn.SetReadDeadline(ts.Add(timeout)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -158,7 +147,7 @@ func (c *Conn) Handle() (err error) {
|
||||
switch string(buf4) {
|
||||
case "RTSP":
|
||||
var res *tcp.Response
|
||||
if res, err = tcp.ReadResponse(c.reader); err != nil {
|
||||
if res, err = c.ReadResponse(); err != nil {
|
||||
return
|
||||
}
|
||||
c.Fire(res)
|
||||
@@ -166,13 +155,15 @@ func (c *Conn) Handle() (err error) {
|
||||
|
||||
case "OPTI", "TEAR", "DESC", "SETU", "PLAY", "PAUS", "RECO", "ANNO", "GET_", "SET_":
|
||||
var req *tcp.Request
|
||||
if req, err = tcp.ReadRequest(c.reader); err != nil {
|
||||
if req, err = c.ReadRequest(); err != nil {
|
||||
return
|
||||
}
|
||||
c.Fire(req)
|
||||
continue
|
||||
|
||||
default:
|
||||
c.Fire("RTSP wrong input")
|
||||
|
||||
for i := 0; ; i++ {
|
||||
// search next start symbol
|
||||
if _, err = c.reader.ReadBytes('$'); err != nil {
|
||||
@@ -204,8 +195,6 @@ func (c *Conn) Handle() (err error) {
|
||||
return fmt.Errorf("RTSP wrong input")
|
||||
}
|
||||
}
|
||||
|
||||
c.Fire("RTSP wrong input")
|
||||
}
|
||||
} else {
|
||||
// hope that the odd channels are always RTCP
|
||||
@@ -254,21 +243,106 @@ func (c *Conn) Handle() (err error) {
|
||||
|
||||
c.Fire(msg)
|
||||
}
|
||||
|
||||
if keepaliveDT != 0 && ts.After(keepaliveTS) {
|
||||
req := &tcp.Request{Method: MethodOptions, URL: c.URL}
|
||||
if err = c.WriteRequest(req); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
keepaliveTS = ts.Add(keepaliveDT)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Conn) keepalive() {
|
||||
// TODO: rewrite to RTCP
|
||||
req := &tcp.Request{Method: MethodOptions, URL: c.URL}
|
||||
for {
|
||||
time.Sleep(time.Second * 25)
|
||||
if c.state == StateNone {
|
||||
return
|
||||
}
|
||||
if err := c.Request(req); err != nil {
|
||||
return
|
||||
func (c *Conn) WriteRequest(req *tcp.Request) error {
|
||||
if req.Proto == "" {
|
||||
req.Proto = ProtoRTSP
|
||||
}
|
||||
|
||||
if req.Header == nil {
|
||||
req.Header = make(map[string][]string)
|
||||
}
|
||||
|
||||
c.sequence++
|
||||
// important to send case sensitive CSeq
|
||||
// https://github.com/AlexxIT/go2rtc/issues/7
|
||||
req.Header["CSeq"] = []string{strconv.Itoa(c.sequence)}
|
||||
|
||||
c.auth.Write(req)
|
||||
|
||||
if c.session != "" {
|
||||
req.Header.Set("Session", c.session)
|
||||
}
|
||||
|
||||
if req.Body != nil {
|
||||
val := strconv.Itoa(len(req.Body))
|
||||
req.Header.Set("Content-Length", val)
|
||||
}
|
||||
|
||||
c.Fire(req)
|
||||
|
||||
if err := c.conn.SetWriteDeadline(time.Now().Add(Timeout)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return req.Write(c.conn)
|
||||
}
|
||||
|
||||
func (c *Conn) ReadRequest() (*tcp.Request, error) {
|
||||
if err := c.conn.SetReadDeadline(time.Now().Add(Timeout)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tcp.ReadRequest(c.reader)
|
||||
}
|
||||
|
||||
func (c *Conn) WriteResponse(res *tcp.Response) error {
|
||||
if res.Proto == "" {
|
||||
res.Proto = ProtoRTSP
|
||||
}
|
||||
|
||||
if res.Status == "" {
|
||||
res.Status = "200 OK"
|
||||
}
|
||||
|
||||
if res.Header == nil {
|
||||
res.Header = make(map[string][]string)
|
||||
}
|
||||
|
||||
if res.Request != nil && res.Request.Header != nil {
|
||||
seq := res.Request.Header.Get("CSeq")
|
||||
if seq != "" {
|
||||
res.Header.Set("CSeq", seq)
|
||||
}
|
||||
}
|
||||
|
||||
if c.session != "" {
|
||||
if res.Request != nil && res.Request.Method == MethodSetup {
|
||||
res.Header.Set("Session", c.session+";timeout=60")
|
||||
} else {
|
||||
res.Header.Set("Session", c.session)
|
||||
}
|
||||
}
|
||||
|
||||
if res.Body != nil {
|
||||
val := strconv.Itoa(len(res.Body))
|
||||
res.Header.Set("Content-Length", val)
|
||||
}
|
||||
|
||||
c.Fire(res)
|
||||
|
||||
if err := c.conn.SetWriteDeadline(time.Now().Add(Timeout)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return res.Write(c.conn)
|
||||
}
|
||||
|
||||
func (c *Conn) ReadResponse() (*tcp.Response, error) {
|
||||
if err := c.conn.SetReadDeadline(time.Now().Add(Timeout)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tcp.ReadResponse(c.reader)
|
||||
}
|
||||
|
||||
+21
-4
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
||||
"github.com/pion/rtp"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (c *Conn) GetMedias() []*core.Media {
|
||||
@@ -28,10 +29,21 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv
|
||||
|
||||
switch c.mode {
|
||||
case core.ModeActiveProducer: // backchannel
|
||||
if channel, err = c.SetupMedia(media, true); err != nil {
|
||||
c.stateMu.Lock()
|
||||
defer c.stateMu.Unlock()
|
||||
|
||||
if c.state == StatePlay {
|
||||
if err = c.Reconnect(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if channel, err = c.SetupMedia(media); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.state = StateSetup
|
||||
|
||||
case core.ModePassiveConsumer:
|
||||
channel = byte(len(c.senders)) * 2
|
||||
|
||||
@@ -46,21 +58,22 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv
|
||||
|
||||
// save original codec to sender (can have Codec.Name = ANY)
|
||||
sender := core.NewSender(media, codec)
|
||||
sender.Handler = c.packetWriter(codec, channel)
|
||||
// important to send original codec for valid IsRTP check
|
||||
sender.Handler = c.packetWriter(track.Codec, channel, codec.PayloadType)
|
||||
sender.HandleRTP(track)
|
||||
|
||||
c.senders = append(c.senders, sender)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) packetWriter(codec *core.Codec, channel uint8) core.HandlerFunc {
|
||||
func (c *Conn) packetWriter(codec *core.Codec, channel, payloadType uint8) core.HandlerFunc {
|
||||
handlerFunc := func(packet *rtp.Packet) {
|
||||
if c.state == StateNone {
|
||||
return
|
||||
}
|
||||
|
||||
clone := *packet
|
||||
clone.Header.PayloadType = codec.PayloadType
|
||||
clone.Header.PayloadType = payloadType
|
||||
|
||||
size := clone.MarshalSize()
|
||||
|
||||
@@ -76,6 +89,10 @@ func (c *Conn) packetWriter(codec *core.Codec, channel uint8) core.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.conn.SetWriteDeadline(time.Now().Add(Timeout)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
n, err := c.conn.Write(data)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
+15
-11
@@ -2,13 +2,15 @@ package rtsp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtcp"
|
||||
"github.com/pion/sdp/v3"
|
||||
"io"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtcp"
|
||||
"github.com/pion/sdp/v3"
|
||||
)
|
||||
|
||||
type RTCP struct {
|
||||
@@ -23,12 +25,6 @@ s=-
|
||||
t=0 0`
|
||||
|
||||
func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) {
|
||||
// fix bug from Reolink Doorbell
|
||||
if i := bytes.Index(rawSDP, []byte("a=sendonlym=")); i > 0 {
|
||||
rawSDP = append(rawSDP[:i+11], rawSDP[i+10:]...)
|
||||
rawSDP[i+10] = '\n'
|
||||
}
|
||||
|
||||
sd := &sdp.SessionDescription{}
|
||||
if err := sd.Unmarshal(rawSDP); err != nil {
|
||||
// fix multiple `s=` https://github.com/AlexxIT/WebRTC/issues/417
|
||||
@@ -38,10 +34,18 @@ func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) {
|
||||
// fix SDP header for some cameras
|
||||
if i := bytes.Index(rawSDP, []byte("\nm=")); i > 0 {
|
||||
rawSDP = append([]byte(sdpHeader), rawSDP[i:]...)
|
||||
sd = &sdp.SessionDescription{}
|
||||
err = sd.Unmarshal(rawSDP)
|
||||
}
|
||||
|
||||
// Fix invalid media type (errSDPInvalidValue) caused by
|
||||
// some TP-LINK IP camera, e.g. TL-IPC44GW
|
||||
rawSDP = bytes.ReplaceAll(rawSDP, []byte("m=application/TP-LINK "), []byte("m=application "))
|
||||
|
||||
if err == io.EOF {
|
||||
rawSDP = append(rawSDP, '\n')
|
||||
}
|
||||
|
||||
sd = &sdp.SessionDescription{}
|
||||
err = sd.Unmarshal(rawSDP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+87
-23
@@ -2,7 +2,7 @@ package rtsp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
@@ -15,51 +15,86 @@ func (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, e
|
||||
}
|
||||
}
|
||||
|
||||
switch c.state {
|
||||
case StateConn, StateSetup:
|
||||
default:
|
||||
return nil, fmt.Errorf("RTSP GetTrack from wrong state: %s", c.state)
|
||||
c.stateMu.Lock()
|
||||
defer c.stateMu.Unlock()
|
||||
|
||||
if c.state == StatePlay {
|
||||
if err := c.Reconnect(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
channel, err := c.SetupMedia(media, true)
|
||||
channel, err := c.SetupMedia(media)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.state = StateSetup
|
||||
|
||||
track := core.NewReceiver(media, codec)
|
||||
track.ID = byte(channel)
|
||||
track.ID = channel
|
||||
c.receivers = append(c.receivers, track)
|
||||
|
||||
return track, nil
|
||||
}
|
||||
|
||||
func (c *Conn) Start() error {
|
||||
switch c.mode {
|
||||
case core.ModeActiveProducer:
|
||||
if err := c.Play(); err != nil {
|
||||
return err
|
||||
func (c *Conn) Start() (err error) {
|
||||
core.Assert(c.mode == core.ModeActiveProducer || c.mode == core.ModePassiveProducer)
|
||||
|
||||
for {
|
||||
ok := false
|
||||
|
||||
c.stateMu.Lock()
|
||||
switch c.state {
|
||||
case StateNone:
|
||||
err = nil
|
||||
case StateConn:
|
||||
err = errors.New("start from CONN state")
|
||||
case StateSetup:
|
||||
switch c.mode {
|
||||
case core.ModeActiveProducer:
|
||||
err = c.Play()
|
||||
case core.ModePassiveProducer:
|
||||
err = nil
|
||||
default:
|
||||
err = errors.New("start from wrong mode: " + c.mode.String())
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
c.state = StatePlay
|
||||
ok = true
|
||||
}
|
||||
}
|
||||
case core.ModePassiveProducer:
|
||||
default:
|
||||
return fmt.Errorf("start wrong mode: %d", c.mode)
|
||||
}
|
||||
c.stateMu.Unlock()
|
||||
|
||||
if err := c.Handle(); c.state != StateNone {
|
||||
_ = c.conn.Close()
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
// Handler can return different states:
|
||||
// 1. None after PLAY should exit without error
|
||||
// 2. Play after PLAY should exit from Start with error
|
||||
// 3. Setup after PLAY should Play once again
|
||||
err = c.Handle()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) Stop() error {
|
||||
func (c *Conn) Stop() (err error) {
|
||||
for _, receiver := range c.receivers {
|
||||
receiver.Close()
|
||||
}
|
||||
for _, sender := range c.senders {
|
||||
sender.Close()
|
||||
}
|
||||
return c.Close()
|
||||
|
||||
c.stateMu.Lock()
|
||||
if c.state != StateNone {
|
||||
c.state = StateNone
|
||||
err = c.Close()
|
||||
}
|
||||
c.stateMu.Unlock()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Conn) MarshalJSON() ([]byte, error) {
|
||||
@@ -82,3 +117,32 @@ func (c *Conn) MarshalJSON() ([]byte, error) {
|
||||
|
||||
return json.Marshal(info)
|
||||
}
|
||||
|
||||
func (c *Conn) Reconnect() error {
|
||||
c.Fire("RTSP reconnect")
|
||||
|
||||
// close current session
|
||||
_ = c.Close()
|
||||
|
||||
// start new session
|
||||
if err := c.Dial(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.Describe(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// restore previous medias
|
||||
for _, receiver := range c.receivers {
|
||||
if _, err := c.SetupMedia(receiver.Media); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, sender := range c.senders {
|
||||
if _, err := c.SetupMedia(sender.Media); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+11
-43
@@ -1,10 +1,7 @@
|
||||
package rtsp
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -79,63 +76,34 @@ a=fmtp:96 packetization-mode=1;profile-level-id=64001F;sprop-parameter-sets=Z0IA
|
||||
|
||||
func TestBugSDP3(t *testing.T) {
|
||||
s := `v=0
|
||||
o=- 1675775048103026 1 IN IP4 192.168.1.123
|
||||
o=- 1680614126554766 1 IN IP4 192.168.0.3
|
||||
s=Session streamed by "preview"
|
||||
t=0 0
|
||||
a=tool:LIVE555 Streaming Media v2020.08.12
|
||||
a=tool:BC Streaming Media v202210012022.10.01
|
||||
a=type:broadcast
|
||||
a=control:*
|
||||
a=range:npt=0-
|
||||
a=range:npt=now-
|
||||
a=x-qt-text-nam:Session streamed by "preview"
|
||||
m=video 0 RTP/AVP 96
|
||||
c=IN IP4 0.0.0.0
|
||||
b=AS:8192
|
||||
a=rtpmap:96 H264/90000
|
||||
a=range:npt=now-
|
||||
a=fmtp:96 packetization-mode=1;profile-level-id=640033;sprop-parameter-sets=Z2QAM6wVFKAoAPGQ,aO48sA==
|
||||
a=recvonly
|
||||
a=control:track1
|
||||
m=audio 0 RTP/AVP 8
|
||||
a=control:track2
|
||||
a=rtpmap:8 PCMA/8000
|
||||
a=sendonlym=audio 0 RTP/AVP 98
|
||||
m=audio 0 RTP/AVP 97
|
||||
c=IN IP4 0.0.0.0
|
||||
b=AS:8192
|
||||
a=rtpmap:98 MPEG4-GENERIC/16000
|
||||
a=fmtp:98 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408;
|
||||
a=rtpmap:97 MPEG4-GENERIC/16000
|
||||
a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408;
|
||||
a=recvonly
|
||||
a=control:track2
|
||||
m=audio 0 RTP/AVP 8
|
||||
a=control:track3
|
||||
`
|
||||
a=rtpmap:8 PCMA/8000
|
||||
a=sendonly`
|
||||
medias, err := UnmarshalSDP([]byte(s))
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, medias, 3)
|
||||
}
|
||||
|
||||
func TestBugSDP4(t *testing.T) {
|
||||
s := `v=0
|
||||
o=- 1676583297494652 1676583297494652 IN IP4 192.168.1.58
|
||||
s=Media Presentation
|
||||
e=NONE
|
||||
b=AS:5050
|
||||
t=0 0
|
||||
a=control:rtsp://192.168.1.58:554/h264_stream/
|
||||
m=video 0 RTP/AVP 96
|
||||
b=AS:5000
|
||||
a=control:rtsp://192.168.1.58:554/h264_stream/trackID=1
|
||||
a=rtpmap:96 H265/90000
|
||||
a=fmtp:96 profile-level-id=420029; packetization-mode=1; sprop-parameter-sets=
|
||||
a=Media_header:MEDIAINFO=494D4B48010100000400050000000000000000000000000000000000000000000000000000000000;
|
||||
a=appversion:1.0
|
||||
`
|
||||
s = strings.ReplaceAll(s, "\n", "\r\n")
|
||||
medias, err := UnmarshalSDP([]byte(s))
|
||||
assert.Nil(t, err)
|
||||
|
||||
codec := medias[0].Codecs[0]
|
||||
assert.Equal(t, core.CodecH264, codec.Name)
|
||||
|
||||
sps, _ := h264.GetParameterSet(codec.FmtpLine)
|
||||
assert.Nil(t, sps)
|
||||
|
||||
profile := h264.GetProfileLevelID(codec.FmtpLine)
|
||||
assert.Equal(t, "420029", profile)
|
||||
}
|
||||
|
||||
+16
-16
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -25,7 +26,7 @@ func (c *Conn) Auth(username, password string) {
|
||||
|
||||
func (c *Conn) Accept() error {
|
||||
for {
|
||||
req, err := tcp.ReadRequest(c.reader)
|
||||
req, err := c.ReadRequest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -39,10 +40,11 @@ func (c *Conn) Accept() error {
|
||||
|
||||
if !c.auth.Validate(req) {
|
||||
res := &tcp.Response{
|
||||
Status: "401 Unauthorized",
|
||||
Header: map[string][]string{"Www-Authenticate": {`Basic realm="go2rtc"`}},
|
||||
Status: "401 Unauthorized",
|
||||
Header: map[string][]string{"Www-Authenticate": {`Basic realm="go2rtc"`}},
|
||||
Request: req,
|
||||
}
|
||||
if err = c.Response(res); err != nil {
|
||||
if err = c.WriteResponse(res); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
@@ -58,7 +60,7 @@ func (c *Conn) Accept() error {
|
||||
},
|
||||
Request: req,
|
||||
}
|
||||
if err = c.Response(res); err != nil {
|
||||
if err = c.WriteResponse(res); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -83,7 +85,7 @@ func (c *Conn) Accept() error {
|
||||
c.Fire(MethodAnnounce)
|
||||
|
||||
res := &tcp.Response{Request: req}
|
||||
if err = c.Response(res); err != nil {
|
||||
if err = c.WriteResponse(res); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -96,7 +98,7 @@ func (c *Conn) Accept() error {
|
||||
Status: "404 Not Found",
|
||||
Request: req,
|
||||
}
|
||||
return c.Response(res)
|
||||
return c.WriteResponse(res)
|
||||
}
|
||||
|
||||
res := &tcp.Response{
|
||||
@@ -108,11 +110,12 @@ func (c *Conn) Accept() error {
|
||||
|
||||
// convert tracks to real output medias medias
|
||||
var medias []*core.Media
|
||||
for _, track := range c.senders {
|
||||
for i, track := range c.senders {
|
||||
media := &core.Media{
|
||||
Kind: core.GetKind(track.Codec.Name),
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{track.Codec},
|
||||
ID: "trackID=" + strconv.Itoa(i),
|
||||
}
|
||||
medias = append(medias, media)
|
||||
}
|
||||
@@ -122,7 +125,7 @@ func (c *Conn) Accept() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = c.Response(res); err != nil {
|
||||
if err = c.WriteResponse(res); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -136,27 +139,24 @@ func (c *Conn) Accept() error {
|
||||
|
||||
const transport = "RTP/AVP/TCP;unicast;interleaved="
|
||||
if strings.HasPrefix(tr, transport) {
|
||||
c.Session = "1" // TODO: fixme
|
||||
c.session = core.RandString(8, 10)
|
||||
c.state = StateSetup
|
||||
res.Header.Set("Transport", tr[:len(transport)+3])
|
||||
} else {
|
||||
res.Status = "461 Unsupported transport"
|
||||
}
|
||||
|
||||
if err = c.Response(res); err != nil {
|
||||
if err = c.WriteResponse(res); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case MethodRecord, MethodPlay:
|
||||
res := &tcp.Response{Request: req}
|
||||
if err = c.Response(res); err == nil {
|
||||
c.state = StatePlay
|
||||
}
|
||||
return err
|
||||
return c.WriteResponse(res)
|
||||
|
||||
case MethodTeardown:
|
||||
res := &tcp.Response{Request: req}
|
||||
_ = c.Response(res)
|
||||
_ = c.WriteResponse(res)
|
||||
c.state = StateNone
|
||||
return c.conn.Close()
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func RemoteAddr(r *http.Request) string {
|
||||
if remote := r.Header.Get("X-Forwarded-For"); remote != "" {
|
||||
return remote + ", " + r.RemoteAddr
|
||||
}
|
||||
return r.RemoteAddr
|
||||
}
|
||||
+15
-4
@@ -5,11 +5,12 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/AlexxIT/go2rtc/pkg/pcm"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
func (c *Conn) GetMedias() []*core.Media {
|
||||
return c.medias
|
||||
return WithResampling(c.medias)
|
||||
}
|
||||
|
||||
func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||
@@ -31,15 +32,16 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv
|
||||
}
|
||||
|
||||
localTrack := c.getTranseiver(media.ID).Sender().Track().(*Track)
|
||||
payloadType := codec.PayloadType
|
||||
|
||||
sender := core.NewSender(media, track.Codec)
|
||||
sender := core.NewSender(media, codec)
|
||||
sender.Handler = func(packet *rtp.Packet) {
|
||||
c.send += packet.MarshalSize()
|
||||
//important to send with remote PayloadType
|
||||
_ = localTrack.WriteRTP(codec.PayloadType, packet)
|
||||
_ = localTrack.WriteRTP(payloadType, packet)
|
||||
}
|
||||
|
||||
switch codec.Name {
|
||||
switch track.Codec.Name {
|
||||
case core.CodecH264:
|
||||
sender.Handler = h264.RTPPay(1200, sender.Handler)
|
||||
if track.Codec.IsRTP() {
|
||||
@@ -55,6 +57,15 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv
|
||||
if track.Codec.IsRTP() {
|
||||
sender.Handler = h265.RTPDepay(track.Codec, sender.Handler)
|
||||
}
|
||||
|
||||
case core.CodecPCMA, core.CodecPCMU, core.CodecPCM:
|
||||
if codec.ClockRate == 0 {
|
||||
if codec.Name == core.CodecPCM {
|
||||
codec.Name = core.CodecPCMA
|
||||
}
|
||||
codec.ClockRate = 8000
|
||||
sender.Handler = pcm.Resample(track.Codec, 8000, sender.Handler)
|
||||
}
|
||||
}
|
||||
|
||||
sender.HandleRTP(track)
|
||||
|
||||
@@ -52,6 +52,53 @@ func UnmarshalMedias(descriptions []*sdp.MediaDescription) (medias []*core.Media
|
||||
return
|
||||
}
|
||||
|
||||
func WithResampling(medias []*core.Media) []*core.Media {
|
||||
for _, media := range medias {
|
||||
if media.Kind != core.KindAudio || media.Direction != core.DirectionSendonly {
|
||||
continue
|
||||
}
|
||||
|
||||
var pcma, pcmu, pcm *core.Codec
|
||||
|
||||
for _, codec := range media.Codecs {
|
||||
switch codec.Name {
|
||||
case core.CodecPCMA:
|
||||
if codec.ClockRate != 0 {
|
||||
pcma = codec
|
||||
} else {
|
||||
pcma = nil
|
||||
}
|
||||
case core.CodecPCMU:
|
||||
if codec.ClockRate != 0 {
|
||||
pcmu = codec
|
||||
} else {
|
||||
pcmu = nil
|
||||
}
|
||||
case core.CodecPCM:
|
||||
pcm = codec
|
||||
}
|
||||
}
|
||||
|
||||
if pcma != nil {
|
||||
pcma = pcma.Clone()
|
||||
pcma.ClockRate = 0 // reset clock rate so will match any
|
||||
media.Codecs = append(media.Codecs, pcma)
|
||||
}
|
||||
if pcmu != nil {
|
||||
pcmu = pcmu.Clone()
|
||||
pcmu.ClockRate = 0
|
||||
media.Codecs = append(media.Codecs, pcmu)
|
||||
}
|
||||
if pcma != nil && pcm == nil {
|
||||
pcm = pcma.Clone()
|
||||
pcm.Name = core.CodecPCM
|
||||
media.Codecs = append(media.Codecs, pcm)
|
||||
}
|
||||
}
|
||||
|
||||
return medias
|
||||
}
|
||||
|
||||
func NewCandidate(network, address string) (string, error) {
|
||||
i := strings.LastIndexByte(address, ':')
|
||||
if i < 0 {
|
||||
|
||||
+2
-1
@@ -112,7 +112,8 @@
|
||||
ev.preventDefault();
|
||||
|
||||
const url = new URL("api/streams", location.href);
|
||||
url.searchParams.set("src", ev.target.dataset.name);
|
||||
const src = decodeURIComponent(ev.target.dataset.name);
|
||||
url.searchParams.set("src", src);
|
||||
fetch(url, {method: "DELETE"}).then(reload);
|
||||
});
|
||||
|
||||
|
||||
+7
-5
@@ -67,12 +67,14 @@
|
||||
|
||||
<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="stream.html?src=${src}&mode=mse">stream.html</a> MSE stream / browsers: Chrome, Firefox, Safari Mac/iPad / codecs: H264, H265*, AAC, PCMA*, PCMU*, PCM* / +OPUS in Chrome and Firefox</li>
|
||||
<li><a href="api/stream.mp4?src=${src}">stream.mp4</a> legacy MP4 stream with AAC audio / browsers: Chrome, Firefox / codecs: H264, H265*, AAC</li>
|
||||
<li><a href="api/stream.mp4?src=${src}&mp4=flac">stream.mp4</a> modern MP4 stream with common audio / browsers: Chrome, Firefox / codecs: H264, H265*, AAC, FLAC (PCMA, PCMU, PCM)</li>
|
||||
<li><a href="api/stream.mp4?src=${src}&mp4=all">stream.mp4</a> MP4 stream with any audio / browsers: Chrome / codecs: H264, H265*, AAC, OPUS, MP3, FLAC (PCMA, PCMU, PCM)</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="api/stream.m3u8?src=${src}">stream.m3u8</a> legacy HLS/TS / browsers: Safari all, Chrome Android / codecs: H264</li>
|
||||
<li><a href="api/stream.m3u8?src=${src}&mp4">stream.m3u8</a> legacy HLS/fMP4 / browsers: Safari all, Chrome Android / codecs: H264, H265*, AAC</li>
|
||||
<li><a href="api/stream.m3u8?src=${src}&mp4=flac">stream.m3u8</a> modern HLS/fMP4 / browsers: Safari all, Chrome Android / codecs: H264, H265*, AAC, FLAC (PCMA, PCMU, PCM)</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>
|
||||
|
||||
+2
-1
@@ -27,7 +27,8 @@ 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
|
||||
"opus", // OPUS Chrome
|
||||
"flac", // FLAC (PCM compatible)
|
||||
"opus", // OPUS Chrome, Firefox
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user