Compare commits

..
Author SHA1 Message Date
LogalDeveloper 8d7a47293c Added per-codec bitstream filter support to audio codecs. 2026-04-04 19:49:37 -04:00
LogalDeveloper 46b9c8bd2e Added codec-aware encoding profiles and moved pixel format to per-variant output.
Encoders now receive the codec being used, allowing them to adjust their flags accordingly. For example, the software encoder gates x264-specific parameters behind a codec check so that future encoders like libx265 or libsvtav1 can provide their own flags without conflict. Pixel format is now specified per variant with a stream specifier rather than globally, enabling codecs like AV1 to use 10-bit color for improved compression efficiency while H.264 variants continue using yuv420p.
2026-04-04 14:50:29 -04:00
LogalDeveloper 50e14adac2 Rearchitected transcoding to treat codecs and encoders as separate concepts.
ActivityPub Tests / federation-test (push) Failing after 4s
ActivityPub Tests / chat-sanitization-test (push) Failing after 3s
ActivityPub Tests / follower-validation-test (push) Failing after 4s
Extract Default Translations / extract-and-commit (push) Successful in 4m21s
Automated API tests / test (push) Has been cancelled
Browser Tests / cypress-run (push) Has been cancelled
Chromatic / chromatic-deployment (push) Has been cancelled
CSS Lint and Formatting / css-lint (push) Has been cancelled
javascript-packages / Generate API Documentation (push) Has been cancelled
Lint / Go linter (push) Has been cancelled
Go Tests / test (push) Has been cancelled
Go Tests / test-bsds (map[name:freebsd version:12.2]) (push) Has been cancelled
Go Tests / test-bsds (map[name:openbsd version:6.8]) (push) Has been cancelled
HLS tests / tests (push) Has been cancelled
2026-04-04 14:11:49 -04:00
Owncast e699bef636 Bundle embedded web app
Lint / GitHub actions (push) Successful in 13s
Automated API tests / test (push) Failing after 7s
Browser Tests / cypress-run (push) Failing after 6s
Chromatic / chromatic-deployment (push) Failing after 4s
CodeQL / Analyze (go) (push) Failing after 58s
CodeQL / Analyze (javascript) (push) Failing after 47s
Build development container / Earthly (push) Successful in 29s
CSS Lint and Formatting / css-lint (push) Successful in 17s
Lint / Go linter (push) Failing after 6s
HLS tests / tests (push) Failing after 6s
Javascript / Code formatting (push) Failing after 6s
Javascript / Test for unused code (push) Failing after 5s
Javascript / Build and bundle web project (push) Has been skipped
Go Tests / test (push) Has been cancelled
Go Tests / test-bsds (map[name:freebsd version:12.2]) (push) Has been cancelled
Go Tests / test-bsds (map[name:openbsd version:6.8]) (push) Has been cancelled
2026-04-04 17:02:24 +00:00
renovate[bot]andGitHub 854674b5fb fix(deps): update dependency react-virtuoso to v4.18.4 2026-04-04 09:19:26 +00:00
renovate[bot]andGitHub 873d7e0a5f fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.40 2026-04-03 16:45:09 +00:00
Gabe Kangas 519d239a87 chore(ci): post a comment if the PR template is invalid 2026-04-03 09:43:16 -07:00
renovate[bot]andGitHub c13eaf25dd fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.39 2026-04-03 05:05:51 +00:00
Owncast 324bf75f69 Bundle embedded web app 2026-04-02 21:57:22 +00:00
renovate[bot]andGitHub 2c64efe505 chore(deps): update typescript-eslint monorepo to v8.58.0 2026-04-02 17:48:38 +00:00
Owncast 722226e054 Bundle embedded web app 2026-04-02 17:46:37 +00:00
renovate[bot]andGitHub d10fc7f5d4 fix(deps): update dependency lodash to v4.18.1 [security] 2026-04-02 13:58:14 +00:00
601553c582 chore(i18n): update translations from Crowdin (#4862)
Co-authored-by: Owncast <owncast@owncast.online>
2026-04-01 11:39:48 -07:00
renovate[bot]andGitHub 8b5fe84e62 chore(deps): update peter-evans/create-or-update-comment digest to 5723223 2026-04-01 09:42:10 +00:00
33efafecd5 feat: implement custom accessible tooltips with keyboard shortcuts for the video player (#4787)
* feat(video-player): add keyboard shortcut tooltips to control bar

- Display keyboard shortcuts (e.g., "Play (Space)") in button tooltips.
- Inject shortcut suffixes into Video.js language strings dynamically.
- Add CSS to style `.vjs-control-text` as a floating tooltip on hover.
- Enable `noUITitleAttributes` to disable native browser tooltips and prevent duplication.

* fix(player-control-tooltips): Prevent player control tooltips from displaying incorrectly in mobile

- Implement mobile touch detection to set 'player-data-is-touch' attribute on body, preventing sticky hover states.

* chore(linting): fix linting and styling errors in VideoJS

* Improve hover detection for tooltips

---------

Co-authored-by: Gabe Kangas <gabek@real-ity.com>
2026-03-31 20:38:03 -07:00
John CostaandGitHub 8ccec822b4 fix(web): set page title on chat embed pages (#4820)
Set a translated page title on the readonly and readwrite chat embed
pages so popout/embedded chat windows display a meaningful title
based on the configured stream name, addressing #4794.

Uses next-export-i18n with a new chatEmbedTitle key for proper
localization. Also fixes the readonly embed error boundary
componentName from ReadWriteChatEmbed to ReadOnlyChatEmbed.
2026-03-31 20:36:28 -07:00
Gabe KangasandGitHub e738156fd7 Sanitize actor displaynames (#4864)
* Sanitize AP name possible XSS

* Pull out sanitizing method + remove rendering markdown for AP chat messages

* Add tests around AP message rendering and usernames

* Add sanitization tests to AP integration test
2026-03-31 20:33:05 -07:00
Gabe Kangas a2981903c2 Move activitypub automated test to run under a container 2026-03-30 12:32:04 -07:00
renovate[bot]andGitHub 5d4fcf9686 fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.38 2026-03-30 05:36:32 +00:00
Owncast 50854f09b3 Bundle embedded web app 2026-03-29 02:02:57 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
54104a5c3c fix(deps): update dependency @uiw/react-codemirror to v4.25.9 (#4859)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-29 01:59:06 +00:00
Owncast 715c267079 Bundle embedded web app 2026-03-28 21:51:18 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
c48cc989f4 fix(deps): update dependency @uiw/codemirror-theme-bbedit to v4.25.9 (#4858)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-28 21:47:19 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
44970293f8 fix(deps): update module github.com/oapi-codegen/runtime to v1.3.1 (#4852)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-28 01:26:54 +00:00
Owncast 423ddb8e22 Bundle embedded web app 2026-03-28 00:21:50 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c593ed381d chore(deps-dev): bump picomatch in /test/automated/api (#4850)
Bumps [picomatch](https://github.com/micromatch/picomatch) from 2.3.1 to 2.3.2.
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 17:08:17 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b6a39721dd chore(deps-dev): bump picomatch in /test/automated/hls (#4848)
Bumps [picomatch](https://github.com/micromatch/picomatch) from 2.3.1 to 2.3.2.
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 17:07:52 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
159e24438d chore(deps-dev): bump flatted from 3.3.3 to 3.4.2 in /web (#4836)
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.3 to 3.4.2.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 17:07:21 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2c3e8fa3c1 chore(deps-dev): bump handlebars from 4.7.8 to 4.7.9 in /web (#4854)
Bumps [handlebars](https://github.com/handlebars-lang/handlebars.js) from 4.7.8 to 4.7.9.
- [Release notes](https://github.com/handlebars-lang/handlebars.js/releases)
- [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/v4.7.9/release-notes.md)
- [Commits](https://github.com/handlebars-lang/handlebars.js/compare/v4.7.8...v4.7.9)

---
updated-dependencies:
- dependency-name: handlebars
  dependency-version: 4.7.9
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 17:06:57 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ba8ce8bf66 chore(deps-dev): bump brace-expansion from 1.1.12 to 1.1.13 in /web (#4856)
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.12 to 1.1.13.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.12...v1.1.13)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.13
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 17:06:21 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
84341875b7 chore(deps): bump brace-expansion in /test/load (#4857)
Bumps  and [brace-expansion](https://github.com/juliangruber/brace-expansion). These dependencies needed to be updated together.

Updates `brace-expansion` from 2.0.2 to 2.0.3
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v2.0.2...v2.0.3)

Updates `brace-expansion` from 1.1.12 to 1.1.13
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v2.0.2...v2.0.3)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 2.0.3
  dependency-type: indirect
- dependency-name: brace-expansion
  dependency-version: 1.1.13
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 17:06:01 -07:00
Owncast d52911160d Bundle embedded web app 2026-03-27 23:45:57 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4ef036b0f4 chore(deps): update dependency chromatic to v16 (#4861)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-27 16:37:17 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
eb3184bcfa chore(deps): update chromaui/action action to v16 (#4860)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-27 16:37:00 -07:00
Gabe Kangas 5c9b2333f9 fix(i18n): only submit translations from the develop branch, not feature branches 2026-03-27 15:53:36 -07:00
Gabe Kangas 77c7c08587 fix(api): fix for incorrect error handling of favicon uploads 2026-03-27 15:34:09 -07:00
Owncast d9a424ea79 Bundle embedded web app 2026-03-27 21:46:34 +00:00
826c7e8a9e chore(i18n): update translations from Crowdin (#4829)
Co-authored-by: Owncast <owncast@owncast.online>
2026-03-27 14:25:59 -07:00
Gabe Kangas 3e6f6de032 chore(deps): also ignore dependabot 2026-03-27 14:24:38 -07:00
renovate[bot]andGitHub 3909c506a2 chore(deps): update peter-evans/create-or-update-comment digest to 3e11480 2026-03-27 20:32:16 +00:00
Owncast e9556d44da Bundle embedded web app 2026-03-27 20:30:58 +00:00
Gabe Kangas c85529370b chore(deps): do not run code lint/format on renovate PRs and do not run linter during builds 2026-03-27 13:25:32 -07:00
Owncast 285a722fac Bundle embedded web app 2026-03-27 20:09:55 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
234c696af9 chore(deps): lock file maintenance (#4807)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-27 13:03:35 -07:00
Owncast 177840b94a Bundle embedded web app 2026-03-27 19:01:56 +00:00
renovate[bot]andGitHub 35e684bd10 chore(deps): update dependency cypress to v15.13.0 2026-03-27 15:51:53 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9b4adab8fc chore(deps): bump picomatch in /test/load (#4851)
Bumps  and [picomatch](https://github.com/micromatch/picomatch). These dependencies needed to be updated together.

Updates `picomatch` from 4.0.3 to 4.0.4
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/4.0.3...4.0.4)

Updates `picomatch` from 2.3.1 to 2.3.2
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/4.0.3...4.0.4)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 4.0.4
  dependency-type: indirect
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 08:49:41 -07:00
Owncast d74d1b55f0 Bundle embedded web app 2026-03-27 01:08:56 +00:00
renovate[bot]andGitHub 4d52d7d1fa chore(deps): update dependency handlebars to v4.7.9 [security] 2026-03-26 22:44:48 +00:00
Owncast 02559acc39 Bundle embedded web app 2026-03-26 22:43:57 +00:00
renovate[bot]andGitHub 892fd3d89e chore(deps): update typescript-eslint monorepo to v8.57.2 2026-03-26 21:59:57 +00:00
Owncast 0bb372d8cb Bundle embedded web app 2026-03-26 21:58:26 +00:00
renovate[bot]andGitHub ddc8e9bed0 chore(deps): update dependency chromatic to v15.3.1 2026-03-26 17:59:18 +00:00
renovate[bot]andGitHub 5b2a3e3127 chore(deps): update peter-evans/create-or-update-comment digest to df7d5bf 2026-03-26 00:29:37 +00:00
Owncast a476142f23 Bundle embedded web app 2026-03-25 23:03:22 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ad090a0f86 chore(deps): bump socket.io-parser from 4.2.4 to 4.2.6 in /test/load (#4834)
Bumps [socket.io-parser](https://github.com/socketio/socket.io) from 4.2.4 to 4.2.6.
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/socket.io-parser@4.2.4...socket.io-parser@4.2.6)

---
updated-dependencies:
- dependency-name: socket.io-parser
  dependency-version: 4.2.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-25 15:58:00 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5365cc0325 chore(deps-dev): bump minimatch from 3.1.2 to 3.1.5 in /web (#4812)
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.1.2 to 3.1.5.
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 3.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-25 15:57:21 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
37404aac5c chore(deps): bump google.golang.org/grpc from 1.75.0 to 1.79.3 in /tools (#4832)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.75.0 to 1.79.3.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.75.0...v1.79.3)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.79.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-25 15:56:33 -07:00
181 changed files with 3896 additions and 2647 deletions
@@ -1,57 +0,0 @@
name: ActivityPub Federation Tests
on:
push:
paths:
- "activitypub/**"
- "webserver/**"
- "test/automated/activitypub/**"
- ".github/workflows/activitypub-federation-tests.yaml"
pull_request:
paths:
- "activitypub/**"
- "webserver/**"
- "test/automated/activitypub/**"
- ".github/workflows/activitypub-federation-tests.yaml"
jobs:
federation-test:
runs-on: ubuntu-latest
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: "same_content_newer"
- name: Check out repository code
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "^1.23"
cache: true
- name: Install Caddy
run: |
sudo apt-get update
sudo apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt-get update
sudo apt-get install -y caddy
- name: Install dependencies
run: |
sudo apt-get install -y snac2 ffmpeg
- name: Run setup
run: |
cd test/automated/activitypub
sudo ./setup.sh
- name: Run federation test
run: |
cd test/automated/activitypub
CI=true USER_COUNT=20 ./run.sh
@@ -1,47 +0,0 @@
name: ActivityPub Follower Validation Tests
on:
push:
paths:
- "activitypub/**"
- "config/**"
- "main.go"
- "test/automated/activitypub/**"
- ".github/workflows/activitypub-follower-validation-tests.yaml"
pull_request:
paths:
- "activitypub/**"
- "config/**"
- "main.go"
- "test/automated/activitypub/**"
- ".github/workflows/activitypub-follower-validation-tests.yaml"
jobs:
follower-validation-test:
runs-on: ubuntu-latest
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: "same_content_newer"
- name: Check out repository code
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "^1.23"
cache: true
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y sqlite3 ffmpeg jq
- name: Run follower validation test
timeout-minutes: 15
run: |
cd test/automated/activitypub
./test-follower-validation.sh
+130
View File
@@ -0,0 +1,130 @@
name: ActivityPub Tests
on:
push:
paths:
- "activitypub/**"
- "core/chat/**"
- "config/**"
- "main.go"
- "webserver/**"
- "test/automated/activitypub/**"
- ".github/workflows/activitypub-tests.yaml"
pull_request:
paths:
- "activitypub/**"
- "core/chat/**"
- "config/**"
- "main.go"
- "webserver/**"
- "test/automated/activitypub/**"
- ".github/workflows/activitypub-tests.yaml"
jobs:
federation-test:
runs-on: ubuntu-latest
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: "same_content_newer"
- name: Check out repository code
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "^1.23"
cache: true
- name: Install Caddy
run: |
sudo apt-get update
sudo apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt-get update
sudo apt-get install -y caddy
- name: Install dependencies
run: |
sudo apt-get install -y snac2 ffmpeg
- name: Run setup
run: |
cd test/automated/activitypub
sudo ./setup.sh
- name: Run federation test
run: |
cd test/automated/activitypub
CI=true USER_COUNT=20 ./test-federation.sh
chat-sanitization-test:
runs-on: ubuntu-latest
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: "same_content_newer"
- name: Check out repository code
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "^1.23"
cache: true
- name: Install Caddy
run: |
sudo apt-get update
sudo apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt-get update
sudo apt-get install -y caddy
- name: Install dependencies
run: |
sudo apt-get install -y snac2 ffmpeg
- name: Run setup
run: |
cd test/automated/activitypub
sudo ./setup.sh
- name: Run chat sanitization test
run: |
cd test/automated/activitypub
CI=true ./test-chat-sanitization.sh
follower-validation-test:
runs-on: ubuntu-latest
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: "same_content_newer"
- name: Check out repository code
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "^1.23"
cache: true
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y sqlite3 ffmpeg jq
- name: Run follower validation test
timeout-minutes: 15
run: |
cd test/automated/activitypub
./test-follower-validation.sh
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
issues: write
steps:
- name: Add comment
uses: peter-evans/create-or-update-comment@9143c495e69aa6ad64fa02b7515ccab6bd35aad1
uses: peter-evans/create-or-update-comment@57232238742e38b2ccc27136ce596ccae7ca28b4
with:
issue-number: ${{ github.event.issue.number }}
body: |
+1 -1
View File
@@ -86,7 +86,7 @@ jobs:
- name: Publish to Chromatic
if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' && steps.changed-files-yaml.outputs.src_any_changed == 'true' }}
uses: chromaui/action@v15
uses: chromaui/action@v16
with:
workingDir: web
projectToken: f47410569b62
@@ -38,6 +38,7 @@ on:
jobs:
formatting:
name: Code formatting
if: github.actor != 'renovate[bot]' && github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest
defaults:
run:
@@ -133,6 +134,7 @@ jobs:
unused-code:
name: Test for unused code
if: github.actor != 'renovate[bot]' && github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest
defaults:
run:
@@ -197,7 +199,7 @@ jobs:
web-bundle:
name: Build and bundle web project
runs-on: ubuntu-latest
if: github.repository == 'owncast/owncast'
if: github.repository == 'owncast/owncast' && !cancelled()
needs: [formatting, unused-code]
steps:
- id: skip_check
+24
View File
@@ -15,6 +15,7 @@ on:
permissions:
contents: read
pull-requests: write
jobs:
validate-checklist:
@@ -44,6 +45,7 @@ jobs:
}
- name: Validate required checkboxes
id: validate
if: ${{ steps.membership.outputs.result == 'false' }}
uses: actions/github-script@v8
with:
@@ -55,6 +57,7 @@ jobs:
const match = prBody.match(markerPattern);
if (!match) {
core.setOutput('missing_checklist', 'true');
core.setFailed(
'## PR Checklist Validation Failed\n\n' +
'The required checklist section is missing from your PR description.\n\n' +
@@ -86,3 +89,24 @@ jobs:
} else {
console.log('All required checklist items are checked');
}
- name: Comment on PR about missing checklist
if: ${{ steps.validate.outputs.missing_checklist == 'true' }}
uses: actions/github-script@v8
with:
script: |
const body =
'## PR Checklist Missing\n\n' +
'It looks like the **required checklist section** was removed from your PR description. ' +
'This section is needed for the PR checks to pass.\n\n' +
'Please edit your PR description and restore the checklist from ' +
'[the PR template](https://raw.githubusercontent.com/owncast/owncast/refs/heads/develop/.github/PULL_REQUEST_TEMPLATE.MD). ' +
'Once restored, check off each item to confirm you have completed them.\n\n' +
'The checklist section begins with `<!-- REQUIRED-CHECKLIST:START -->` and ends with `<!-- REQUIRED-CHECKLIST:END -->`.';
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: body
});
+2
View File
@@ -16,6 +16,8 @@ on:
# Run the workflow every hour
- cron: '0 * * * *'
push:
branches:
- develop
paths:
- 'web/i18n/en/translation.json'
- 'web/**/*.tsx'
+13 -4
View File
@@ -4,12 +4,24 @@ import (
"fmt"
"github.com/go-fed/activity/streams/vocab"
"github.com/microcosm-cc/bluemonday"
"github.com/owncast/owncast/activitypub/resolvers"
"github.com/owncast/owncast/core/chat"
"github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/persistence/configrepository"
)
// sanitizeActorName strips HTML tags from the ActivityPub actor display name.
// Falls back to the username if the display name is empty or entirely HTML.
func sanitizeActorName(displayName, username string) string {
strict := bluemonday.StrictPolicy()
name := strict.Sanitize(displayName)
if name == "" {
name = strict.Sanitize(username)
}
return name
}
func handleEngagementActivity(eventType events.EventType, isLiveNotification bool, actorReference vocab.ActivityStreamsActorProperty, action string) error {
configRepository := configrepository.Get()
@@ -30,10 +42,7 @@ func handleEngagementActivity(eventType events.EventType, isLiveNotification boo
}
// Send chat message
actorName := actor.Name
if actorName == "" {
actorName = actor.Username
}
actorName := sanitizeActorName(actor.Name, actor.Username)
actorIRI := actor.ActorIriString()
userPrefix := fmt.Sprintf("%s ", actorName)
+120
View File
@@ -0,0 +1,120 @@
package inbox
import (
"testing"
)
func TestSanitizeActorName(t *testing.T) {
tests := []struct {
name string
displayName string
username string
expected string
}{
{
name: "plain display name",
displayName: "Alice",
username: "alice",
expected: "Alice",
},
{
name: "display name with emoji",
displayName: "Alice 🦊",
username: "alice",
expected: "Alice 🦊",
},
{
name: "display name with unicode",
displayName: "Ålice Böb",
username: "alice",
expected: "Ålice Böb",
},
{
name: "empty display name falls back to username",
displayName: "",
username: "alice",
expected: "alice",
},
{
name: "script tag in display name",
displayName: `<script>alert("xss")</script>`,
username: "alice",
expected: "alice",
},
{
name: "iframe injection in display name",
displayName: `<iframe src="https://evil.com" style="position:fixed;top:0;left:0;width:100%;height:100%"></iframe>`,
username: "alice",
expected: "alice",
},
{
name: "img tag in display name",
displayName: `<img src="https://evil.com/track.png">`,
username: "alice",
expected: "alice",
},
{
name: "form injection in display name",
displayName: `<form action="https://evil.com/steal"><input name="pw" type="password"></form>`,
username: "alice",
expected: "alice",
},
{
name: "meta refresh in display name",
displayName: `<meta http-equiv="refresh" content="0;url=https://evil.com">`,
username: "alice",
expected: "alice",
},
{
name: "mixed text and HTML in display name",
displayName: `Alice <script>alert(1)</script> Bob`,
username: "alice",
expected: "Alice Bob",
},
{
name: "custom emoji HTML in display name",
displayName: `Alice :blobcat: <img src="https://instance.com/emoji/blobcat.png" class="custom-emoji">`,
username: "alice",
expected: "Alice :blobcat: ",
},
{
name: "HTML in both display name and username",
displayName: `<script>alert(1)</script>`,
username: `<b>alice</b>`,
expected: "alice",
},
{
name: "entirely HTML display name falls back to username",
displayName: `<div></div>`,
username: "alice",
expected: "alice",
},
{
name: "style tag in display name",
displayName: `<style>body{display:none}</style>Alice`,
username: "alice",
expected: "Alice",
},
{
name: "nested HTML tags",
displayName: `<div><span><a href="https://evil.com">Click me</a></span></div>`,
username: "alice",
expected: "Click me",
},
{
name: "event handler attributes",
displayName: `<img src=x onerror="alert(1)">Alice`,
username: "alice",
expected: "Alice",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := sanitizeActorName(tt.displayName, tt.username)
if result != tt.expected {
t.Errorf("sanitizeActorName(%q, %q) = %q, want %q", tt.displayName, tt.username, result, tt.expected)
}
})
}
}
+2
View File
@@ -93,6 +93,8 @@ func GetDefaults() Defaults {
VideoBitrate: 1200,
Framerate: 24,
CPUUsageLevel: 2,
VideoCodec: "h264",
AudioCodec: "aac",
},
},
-1
View File
@@ -126,7 +126,6 @@ func SendFediverseAction(eventType string, userAccountName string, image *string
}
message.SetDefaults()
message.RenderBody()
if err := Broadcast(&message); err != nil {
log.Errorln("error sending system message", err)
+10 -41
View File
@@ -1,7 +1,6 @@
package core
import (
"fmt"
"os"
"path"
"path/filepath"
@@ -99,53 +98,23 @@ func createInitialOfflineState() error {
// offline video stream state only. No live stream HLS segments will continue to be
// referenced.
func transitionToOfflineVideoStreamContent() {
log.Traceln("Placing offline fMP4 content into HLS directories")
log.Traceln("Firing transcoder with offline stream state")
configRepository := configrepository.Get()
_transcoder := transcoder.NewTranscoder()
_transcoder.SetIdentifier("offline")
_transcoder.SetLatencyLevel(models.GetLatencyLevel(4))
_transcoder.SetIsEvent(true)
offlineInitPath, offlineSegmentPath, err := saveOfflineFMP4ToDisk()
offlineFilePath, err := saveOfflineClipToDisk("offline-v2.ts")
if err != nil {
log.Fatalln("unable to save offline fMP4 files:", err)
log.Fatalln("unable to save offline clip:", err)
}
variants := configRepository.GetStreamOutputVariants()
if len(variants) == 0 {
// Ensure at least one variant directory exists.
variants = make([]models.StreamOutputVariant, 1)
}
for index := range variants {
variantDir := filepath.Join(config.HLSStoragePath, fmt.Sprintf("%d", index))
if err := os.MkdirAll(variantDir, 0o750); err != nil {
log.Errorln("unable to create variant directory:", err)
continue
}
makeVariantIndexOffline(index, offlineInitPath, offlineSegmentPath)
}
// Write the master playlist that points to each variant's playlist.
masterPlaylistPath := filepath.Join(config.HLSStoragePath, "stream.m3u8")
masterFile, err := os.Create(masterPlaylistPath) //nolint:gosec
if err != nil {
log.Errorln("unable to create master playlist:", err)
} else {
_, _ = masterFile.WriteString("#EXTM3U\n")
_, _ = masterFile.WriteString("#EXT-X-VERSION:7\n")
_, _ = masterFile.WriteString("#EXT-X-INDEPENDENT-SEGMENTS\n")
for index := range variants {
_, _ = fmt.Fprintf(masterFile, "#EXT-X-STREAM-INF:BANDWIDTH=0\n")
_, _ = fmt.Fprintf(masterFile, "%d/stream.m3u8\n", index)
}
if err := masterFile.Close(); err != nil {
log.Errorln("unable to close master playlist:", err)
}
// Notify the storage provider so it can rewrite variant URLs
// to absolute paths for S3 or remote serving endpoints.
_storage.MasterPlaylistWritten(masterPlaylistPath)
}
_transcoder.SetInput(offlineFilePath)
go _transcoder.Start(false)
// Copy the logo to be the thumbnail
configRepository := configrepository.Get()
logo := configRepository.GetLogoPath()
dst := filepath.Join(config.TempDir, "thumbnail.jpg")
if err = utils.Copy(filepath.Join("data", logo), dst); err != nil {
+33 -61
View File
@@ -5,13 +5,14 @@ import (
"os"
"path/filepath"
"github.com/grafov/m3u8"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/static"
"github.com/owncast/owncast/utils"
log "github.com/sirupsen/logrus"
)
func appendOfflineToVariantPlaylist(index int, playlistFilePath string, initFilename string, segmentFilename string) {
func appendOfflineToVariantPlaylist(index int, playlistFilePath string) {
existingPlaylistContents, err := os.ReadFile(playlistFilePath) // nolint: gosec
if err != nil {
log.Debugln("unable to read existing playlist file", err)
@@ -33,10 +34,9 @@ func appendOfflineToVariantPlaylist(index int, playlistFilePath string, initFile
// Manually append the offline clip to the end of the media playlist.
_, _ = atomicWriteTmpPlaylistFile.WriteString("#EXT-X-DISCONTINUITY\n")
_, _ = fmt.Fprintf(atomicWriteTmpPlaylistFile, "#EXT-X-MAP:URI=\"%s\"\n", initFilename)
// If "offline" content gets changed then change the duration below
_, _ = atomicWriteTmpPlaylistFile.WriteString("#EXTINF:8.000000,\n")
_, _ = fmt.Fprintf(atomicWriteTmpPlaylistFile, "%s\n", segmentFilename)
_, _ = atomicWriteTmpPlaylistFile.WriteString("offline-v2.ts\n")
_, _ = atomicWriteTmpPlaylistFile.WriteString("#EXT-X-ENDLIST\n")
if err := atomicWriteTmpPlaylistFile.Close(); err != nil {
@@ -48,89 +48,61 @@ func appendOfflineToVariantPlaylist(index int, playlistFilePath string, initFile
}
}
func makeVariantIndexOffline(index int, offlineInitPath string, offlineSegmentPath string) {
variantDir := filepath.Join(config.HLSStoragePath, fmt.Sprintf("%d", index))
playlistFilePath := filepath.Join(variantDir, "stream.m3u8")
func makeVariantIndexOffline(index int, offlineFilePath string, offlineFilename string) {
playlistFilePath := fmt.Sprintf(filepath.Join(config.HLSStoragePath, "%d/stream.m3u8"), index)
segmentFilePath := fmt.Sprintf(filepath.Join(config.HLSStoragePath, "%d/%s"), index, offlineFilename)
initDest := filepath.Join(variantDir, "offline-init.mp4")
segmentDest := filepath.Join(variantDir, "offline-v2.m4s")
if err := utils.Copy(offlineInitPath, initDest); err != nil {
log.Warnln(err)
return
}
if _, err := _storage.Save(initDest, 0); err != nil {
if err := utils.Copy(offlineFilePath, segmentFilePath); err != nil {
log.Warnln(err)
}
if err := utils.Copy(offlineSegmentPath, segmentDest); err != nil {
log.Warnln(err)
return
}
if _, err := _storage.Save(segmentDest, 0); err != nil {
if _, err := _storage.Save(segmentFilePath, 0); err != nil {
log.Warnln(err)
}
if utils.DoesFileExists(playlistFilePath) {
appendOfflineToVariantPlaylist(index, playlistFilePath, "offline-init.mp4", "offline-v2.m4s")
appendOfflineToVariantPlaylist(index, playlistFilePath)
} else {
createEmptyOfflinePlaylist(playlistFilePath, "offline-init.mp4", "offline-v2.m4s")
createEmptyOfflinePlaylist(playlistFilePath, offlineFilename)
}
if _, err := _storage.Save(playlistFilePath, 0); err != nil {
log.Warnln(err)
}
}
func createEmptyOfflinePlaylist(playlistFilePath string, initFilename string, segmentFilename string) {
func createEmptyOfflinePlaylist(playlistFilePath string, offlineFilename string) {
p, err := m3u8.NewMediaPlaylist(1, 1)
if err != nil {
log.Errorln(err)
}
// If "offline" content gets changed then change the duration below
if err := p.Append(offlineFilename, 8.0, ""); err != nil {
log.Errorln(err)
}
p.Close()
f, err := os.Create(playlistFilePath) //nolint:gosec
if err != nil {
log.Errorln(err)
return
}
defer f.Close()
_, _ = f.WriteString("#EXTM3U\n")
_, _ = f.WriteString("#EXT-X-VERSION:7\n")
_, _ = f.WriteString("#EXT-X-TARGETDURATION:8\n")
_, _ = fmt.Fprintf(f, "#EXT-X-MAP:URI=\"%s\"\n", initFilename)
// If "offline" content gets changed then change the duration below
_, _ = f.WriteString("#EXTINF:8.000000,\n")
_, _ = fmt.Fprintf(f, "%s\n", segmentFilename)
_, _ = f.WriteString("#EXT-X-ENDLIST\n")
if _, err := f.Write(p.Encode().Bytes()); err != nil {
log.Errorln(err)
}
}
func saveOfflineFMP4ToDisk() (initPath string, segmentPath string, err error) {
initData := static.GetOfflineInitSegment()
initTmp, err := os.CreateTemp(config.TempDir, "offline-init-*.mp4")
func saveOfflineClipToDisk(offlineFilename string) (string, error) {
offlineFileData := static.GetOfflineSegment()
offlineTmpFile, err := os.CreateTemp(config.TempDir, offlineFilename)
if err != nil {
return "", "", fmt.Errorf("unable to create temp file for offline init segment: %s", err)
log.Errorln("unable to create temp file for offline video segment", err)
}
if _, err = initTmp.Write(initData); err != nil {
return "", "", fmt.Errorf("unable to write offline init segment to disk: %s", err)
if _, err = offlineTmpFile.Write(offlineFileData); err != nil {
return "", fmt.Errorf("unable to write offline segment to disk: %s", err)
}
if err := initTmp.Close(); err != nil {
return "", "", fmt.Errorf("unable to close offline init segment: %s", err)
}
initPath, _ = filepath.Abs(initTmp.Name())
segData := static.GetOfflineMediaSegment()
segTmp, err := os.CreateTemp(config.TempDir, "offline-v2-*.m4s")
if err != nil {
return "", "", fmt.Errorf("unable to create temp file for offline media segment: %s", err)
}
if _, err = segTmp.Write(segData); err != nil {
return "", "", fmt.Errorf("unable to write offline media segment to disk: %s", err)
}
if err := segTmp.Close(); err != nil {
return "", "", fmt.Errorf("unable to close offline media segment: %s", err)
}
segmentPath, _ = filepath.Abs(segTmp.Name())
return initPath, segmentPath, nil
offlineFilePath := offlineTmpFile.Name()
return filepath.Abs(offlineFilePath)
}
+1 -1
View File
@@ -82,7 +82,7 @@ func getAllFilesRecursive(baseDirectory string) (map[string][]os.FileInfo, error
directory = info.Name()
}
if filepath.Ext(info.Name()) == ".m4s" {
if filepath.Ext(info.Name()) == ".ts" {
files[directory] = append(files[directory], info)
}
+1 -1
View File
@@ -317,7 +317,7 @@ func (s *S3Storage) retrieveAllVideoSegments() ([]s3object, error) {
err := s.s3Client.ListObjectsPages(allObjectsListRequest,
func(page *s3.ListObjectsOutput, lastPage bool) bool {
for _, item := range page.Contents {
if strings.HasSuffix(*item.Key, ".m4s") {
if strings.HasSuffix(*item.Key, ".ts") {
allObjects = append(allObjects, s3object{
key: *item.Key,
lastModified: *item.LastModified,
+4 -2
View File
@@ -96,7 +96,9 @@ func SetStreamAsDisconnected() {
_stats.LastConnectTime = nil
_broadcaster = nil
offlineInitPath, offlineSegmentPath, err := saveOfflineFMP4ToDisk()
offlineFilename := "offline-v2.ts"
offlineFilePath, err := saveOfflineClipToDisk(offlineFilename)
if err != nil {
log.Errorln(err)
return
@@ -120,7 +122,7 @@ func SetStreamAsDisconnected() {
}
for index := range _currentBroadcast.OutputSettings {
makeVariantIndexOffline(index, offlineInitPath, offlineSegmentPath)
makeVariantIndexOffline(index, offlineFilePath, offlineFilename)
}
StartOfflineCleanupTimer()
+49
View File
@@ -0,0 +1,49 @@
package transcoder
const audioCodecAAC = "aac"
// aacFFmpegEncoder happens to match audioCodecAAC, but represents the ffmpeg
// encoder name, which could be "libfdk_aac" or another AAC encoder instead.
const aacFFmpegEncoder = "aac"
// AudioCodec represents an audio compression format (e.g., AAC, Opus).
type AudioCodec interface {
Name() string
DisplayName() string
FFmpegEncoderName() string
BitstreamFilters() []string
}
// AACCodec implements AudioCodec for AAC audio.
type AACCodec struct{}
func (c *AACCodec) Name() string {
return audioCodecAAC
}
func (c *AACCodec) DisplayName() string {
return "AAC"
}
// FFmpegEncoderName returns the ffmpeg encoder to use. We use the built-in
// "aac" encoder rather than "libfdk_aac" since it is universally available.
func (c *AACCodec) FFmpegEncoderName() string {
return aacFFmpegEncoder
}
func (c *AACCodec) BitstreamFilters() []string {
return []string{
// "aac_adtstoasc", // Required for fMP4 segments, not needed yet.
}
}
var audioCodecRegistry = map[string]AudioCodec{
audioCodecAAC: &AACCodec{},
}
func getAudioCodec(name string) AudioCodec {
if codec, ok := audioCodecRegistry[name]; ok {
return codec
}
return &AACCodec{}
}
-549
View File
@@ -1,549 +0,0 @@
//nolint:goconst
package transcoder
import (
"fmt"
"os/exec"
"strings"
log "github.com/sirupsen/logrus"
)
// Codec represents a supported codec on the system.
type Codec interface {
Name() string
DisplayName() string
GlobalFlags() []string
PixelFormat() string
Scaler() string
ExtraArguments() []string
ExtraFilters() string
VariantFlags(v *HLSVariant) []string
GetPresetForLevel(l int) string
}
var supportedCodecs = map[string]string{
(&Libx264Codec{}).Name(): "libx264",
(&OmxCodec{}).Name(): "omx",
(&VaapiCodec{}).Name(): "vaapi",
(&QuicksyncCodec{}).Name(): "qsv",
(&NvencCodec{}).Name(): "NVIDIA nvenc",
(&VideoToolboxCodec{}).Name(): "videotoolbox",
}
// Libx264Codec represents an instance of the Libx264 Codec.
type Libx264Codec struct{}
// Name returns the codec name.
func (c *Libx264Codec) Name() string {
return "libx264"
}
// DisplayName returns the human readable name of the codec.
func (c *Libx264Codec) DisplayName() string {
return "x264"
}
// GlobalFlags are the global flags used with this codec in the transcoder.
func (c *Libx264Codec) GlobalFlags() []string {
return nil
}
// PixelFormat is the pixel format required for this codec.
func (c *Libx264Codec) PixelFormat() string {
return "yuv420p" //nolint:goconst
}
// Scaler is the scaler used for resizing the video in the transcoder.
func (c *Libx264Codec) Scaler() string {
return ""
}
// ExtraArguments are the extra arguments used with this codec in the transcoder.
func (c *Libx264Codec) ExtraArguments() []string {
return []string{
"-tune", "zerolatency", // Option used for good for fast encoding and low-latency streaming (always includes iframes in each segment)
}
}
// ExtraFilters are the extra filters required for this codec in the transcoder.
func (c *Libx264Codec) ExtraFilters() string {
return ""
}
// VariantFlags returns a string representing a single variant processed by this codec.
func (c *Libx264Codec) VariantFlags(v *HLSVariant) []string {
return []string{
fmt.Sprintf("-x264-params:v:%d", v.index),
"scenecut=0:open_gop=0", // How often the encoder checks the bitrate in order to meet average/max values
fmt.Sprintf("-bufsize:v:%d", v.index), fmt.Sprintf("%dk", v.getBufferSize()),
fmt.Sprintf("-profile:v:%d", v.index), "high", // Encoding profile
}
}
// GetPresetForLevel returns the string preset for this codec given an integer level.
func (c *Libx264Codec) GetPresetForLevel(l int) string {
presetMapping := map[int]string{
0: "ultrafast",
1: "superfast",
2: "veryfast",
3: "faster",
4: "fast",
}
preset, ok := presetMapping[l]
if !ok {
defaultPreset := presetMapping[1]
log.Errorf("Invalid level for x264 preset %d, defaulting to %s", l, defaultPreset)
return defaultPreset
}
return preset
}
// OmxCodec represents an instance of the Omx codec.
type OmxCodec struct{}
// Name returns the codec name.
func (c *OmxCodec) Name() string {
return "h264_omx"
}
// DisplayName returns the human readable name of the codec.
func (c *OmxCodec) DisplayName() string {
return "OpenMAX (omx)"
}
// GlobalFlags are the global flags used with this codec in the transcoder.
func (c *OmxCodec) GlobalFlags() []string {
return nil
}
// PixelFormat is the pixel format required for this codec.
func (c *OmxCodec) PixelFormat() string {
return "yuv420p"
}
// Scaler is the scaler used for resizing the video in the transcoder.
func (c *OmxCodec) Scaler() string {
return ""
}
// ExtraArguments are the extra arguments used with this codec in the transcoder.
func (c *OmxCodec) ExtraArguments() []string {
return []string{
"-tune", "zerolatency", // Option used for good for fast encoding and low-latency streaming (always includes iframes in each segment)
}
}
// ExtraFilters are the extra filters required for this codec in the transcoder.
func (c *OmxCodec) ExtraFilters() string {
return ""
}
// VariantFlags returns a string representing a single variant processed by this codec.
func (c *OmxCodec) VariantFlags(v *HLSVariant) []string {
return nil
}
// GetPresetForLevel returns the string preset for this codec given an integer level.
func (c *OmxCodec) GetPresetForLevel(l int) string {
presetMapping := map[int]string{
0: "ultrafast",
1: "superfast",
2: "veryfast",
3: "faster",
4: "fast",
}
preset, ok := presetMapping[l]
if !ok {
defaultPreset := presetMapping[1]
log.Errorf("Invalid level for omx preset %d, defaulting to %s", l, defaultPreset)
return defaultPreset
}
return preset
}
// VaapiCodec represents an instance of the Vaapi codec.
type VaapiCodec struct{}
// Name returns the codec name.
func (c *VaapiCodec) Name() string {
return "h264_vaapi"
}
// DisplayName returns the human readable name of the codec.
func (c *VaapiCodec) DisplayName() string {
return "VA-API"
}
// GlobalFlags are the global flags used with this codec in the transcoder.
func (c *VaapiCodec) GlobalFlags() []string {
flags := []string{
"-hwaccel", "vaapi",
"-hwaccel_output_format", "vaapi",
"-vaapi_device", "/dev/dri/renderD128",
}
return flags
}
// PixelFormat is the pixel format required for this codec.
func (c *VaapiCodec) PixelFormat() string {
return "vaapi"
}
// Scaler is the scaler used for resizing the video in the transcoder.
func (c *VaapiCodec) Scaler() string {
return "scale_vaapi"
}
// ExtraFilters are the extra filters required for this codec in the transcoder.
func (c *VaapiCodec) ExtraFilters() string {
return "hwupload=extra_hw_frames=64,format=vaapi"
}
// ExtraArguments are the extra arguments used with this codec in the transcoder.
func (c *VaapiCodec) ExtraArguments() []string {
return nil
}
// VariantFlags returns a string representing a single variant processed by this codec.
func (c *VaapiCodec) VariantFlags(v *HLSVariant) []string {
return nil
}
// GetPresetForLevel returns the string preset for this codec given an integer level.
func (c *VaapiCodec) GetPresetForLevel(l int) string {
presetMapping := map[int]string{
0: "ultrafast",
1: "superfast",
2: "veryfast",
3: "faster",
4: "fast",
}
preset, ok := presetMapping[l]
if !ok {
defaultPreset := presetMapping[1]
log.Errorf("Invalid level for vaapi preset %d, defaulting to %s", l, defaultPreset)
return defaultPreset
}
return preset
}
// NvencCodec represents an instance of the Nvenc Codec.
type NvencCodec struct{}
// Name returns the codec name.
func (c *NvencCodec) Name() string {
return "h264_nvenc"
}
// DisplayName returns the human readable name of the codec.
func (c *NvencCodec) DisplayName() string {
return "nvidia nvenc"
}
// GlobalFlags are the global flags used with this codec in the transcoder.
func (c *NvencCodec) GlobalFlags() []string {
flags := []string{
"-hwaccel", "cuda",
}
return flags
}
// PixelFormat is the pixel format required for this codec.
func (c *NvencCodec) PixelFormat() string {
return "yuv420p"
}
// Scaler is the scaler used for resizing the video in the transcoder.
func (c *NvencCodec) Scaler() string {
return ""
}
// ExtraArguments are the extra arguments used with this codec in the transcoder.
func (c *NvencCodec) ExtraArguments() []string {
return nil
}
// ExtraFilters are the extra filters required for this codec in the transcoder.
func (c *NvencCodec) ExtraFilters() string {
return ""
}
// VariantFlags returns a string representing a single variant processed by this codec.
func (c *NvencCodec) VariantFlags(v *HLSVariant) []string {
tuning := "ll" // low latency
return []string{
fmt.Sprintf("-tune:v:%d", v.index), tuning,
}
}
// GetPresetForLevel returns the string preset for this codec given an integer level.
func (c *NvencCodec) GetPresetForLevel(l int) string {
presetMapping := map[int]string{
0: "p1",
1: "p2",
2: "p3",
3: "p4",
4: "p5",
}
preset, ok := presetMapping[l]
if !ok {
defaultPreset := presetMapping[2]
log.Errorf("Invalid level for nvenc preset %d, defaulting to %s", l, defaultPreset)
return defaultPreset
}
return preset
}
// QuicksyncCodec represents an instance of the Intel Quicksync Codec.
type QuicksyncCodec struct{}
// Name returns the codec name.
func (c *QuicksyncCodec) Name() string {
return "h264_qsv"
}
// DisplayName returns the human readable name of the codec.
func (c *QuicksyncCodec) DisplayName() string {
return "Intel QuickSync"
}
// GlobalFlags are the global flags used with this codec in the transcoder.
func (c *QuicksyncCodec) GlobalFlags() []string {
flags := []string{
"-init_hw_device", "qsv=hw",
"-filter_hw_device", "hw",
}
return flags
}
// PixelFormat is the pixel format required for this codec.
func (c *QuicksyncCodec) PixelFormat() string {
return "qsv"
}
// Scaler is the scaler used for resizing the video in the transcoder.
func (c *QuicksyncCodec) Scaler() string {
return "scale_qsv"
}
// ExtraArguments are the extra arguments used with this codec in the transcoder.
func (c *QuicksyncCodec) ExtraArguments() []string {
return nil
}
// ExtraFilters are the extra filters required for this codec in the transcoder.
func (c *QuicksyncCodec) ExtraFilters() string {
return "hwupload=extra_hw_frames=64,format=qsv"
}
// VariantFlags returns a string representing a single variant processed by this codec.
func (c *QuicksyncCodec) VariantFlags(v *HLSVariant) []string {
return nil
}
// GetPresetForLevel returns the string preset for this codec given an integer level.
func (c *QuicksyncCodec) GetPresetForLevel(l int) string {
presetMapping := map[int]string{
0: "veryfast",
1: "fast",
2: "medium",
3: "slow",
4: "veryslow",
}
preset, ok := presetMapping[l]
if !ok {
defaultPreset := presetMapping[2]
log.Errorf("Invalid level for quicksync preset %d, defaulting to %s", l, defaultPreset)
return defaultPreset
}
return preset
}
// Video4Linux represents an instance of the V4L Codec.
type Video4Linux struct{}
// Name returns the codec name.
func (c *Video4Linux) Name() string {
return "h264_v4l2m2m"
}
// DisplayName returns the human readable name of the codec.
func (c *Video4Linux) DisplayName() string {
return "Video4Linux"
}
// GlobalFlags are the global flags used with this codec in the transcoder.
func (c *Video4Linux) GlobalFlags() []string {
return nil
}
// PixelFormat is the pixel format required for this codec.
func (c *Video4Linux) PixelFormat() string {
return "nv21"
}
// Scaler is the scaler used for resizing the video in the transcoder.
func (c *Video4Linux) Scaler() string {
return ""
}
// ExtraArguments are the extra arguments used with this codec in the transcoder.
func (c *Video4Linux) ExtraArguments() []string {
return nil
}
// ExtraFilters are the extra filters required for this codec in the transcoder.
func (c *Video4Linux) ExtraFilters() string {
return ""
}
// VariantFlags returns a string representing a single variant processed by this codec.
func (c *Video4Linux) VariantFlags(v *HLSVariant) []string {
return nil
}
// GetPresetForLevel returns the string preset for this codec given an integer level.
func (c *Video4Linux) GetPresetForLevel(l int) string {
presetMapping := map[int]string{
0: "ultrafast",
1: "superfast",
2: "veryfast",
3: "faster",
4: "fast",
}
preset, ok := presetMapping[l]
if !ok {
defaultPreset := presetMapping[1]
log.Errorf("Invalid level for v4l preset %d, defaulting to %s", l, defaultPreset)
return defaultPreset
}
return preset
}
// VideoToolboxCodec represents an instance of the VideoToolbox codec.
type VideoToolboxCodec struct{}
// Name returns the codec name.
func (c *VideoToolboxCodec) Name() string {
return "h264_videotoolbox"
}
// DisplayName returns the human readable name of the codec.
func (c *VideoToolboxCodec) DisplayName() string {
return "VideoToolbox"
}
// GlobalFlags are the global flags used with this codec in the transcoder.
func (c *VideoToolboxCodec) GlobalFlags() []string {
return nil
}
// PixelFormat is the pixel format required for this codec.
func (c *VideoToolboxCodec) PixelFormat() string {
return "nv12"
}
// Scaler is the scaler used for resizing the video in the transcoder.
func (c *VideoToolboxCodec) Scaler() string {
return ""
}
// ExtraFilters are the extra filters required for this codec in the transcoder.
func (c *VideoToolboxCodec) ExtraFilters() string {
return ""
}
// ExtraArguments are the extra arguments used with this codec in the transcoder.
func (c *VideoToolboxCodec) ExtraArguments() []string {
return nil
}
// VariantFlags returns a string representing a single variant processed by this codec.
func (c *VideoToolboxCodec) VariantFlags(v *HLSVariant) []string {
if v.cpuUsageLevel >= 3 {
return nil
}
return []string{"-realtime", "true"}
}
// GetPresetForLevel returns the string preset for this codec given an integer level.
func (c *VideoToolboxCodec) GetPresetForLevel(l int) string {
presetMapping := map[int]string{
0: "ultrafast",
1: "superfast",
2: "veryfast",
3: "faster",
4: "fast",
}
preset, ok := presetMapping[l]
if !ok {
defaultPreset := presetMapping[1]
log.Errorf("Invalid level for videotoolbox preset %d, defaulting to %s", l, defaultPreset)
return defaultPreset
}
return preset
}
// GetCodecs will return the supported codecs available on the system.
func GetCodecs(ffmpegPath string) []string {
codecs := make([]string, 0)
cmd := exec.Command(ffmpegPath, "-encoders")
out, err := cmd.CombinedOutput()
if err != nil {
log.Errorln(err)
return codecs
}
response := string(out)
lines := strings.Split(response, "\n")
for _, line := range lines {
if strings.Contains(line, "H.264") {
fields := strings.Fields(line)
codec := fields[1]
if _, supported := supportedCodecs[codec]; supported {
codecs = append(codecs, codec)
}
}
}
return codecs
}
func getCodec(name string) Codec {
switch name {
case (&NvencCodec{}).Name():
return &NvencCodec{}
case (&VaapiCodec{}).Name():
return &VaapiCodec{}
case (&QuicksyncCodec{}).Name():
return &QuicksyncCodec{}
case (&OmxCodec{}).Name():
return &OmxCodec{}
case (&Video4Linux{}).Name():
return &Video4Linux{}
case (&VideoToolboxCodec{}).Name():
return &VideoToolboxCodec{}
default:
return &Libx264Codec{}
}
}
+118
View File
@@ -0,0 +1,118 @@
package transcoder
import (
"os/exec"
"sort"
"strings"
log "github.com/sirupsen/logrus"
)
// EncoderCodecSupport describes which codecs an encoder supports on this system.
type EncoderCodecSupport struct {
EncoderType string `json:"encoderType"`
EncoderDisplayName string `json:"encoderDisplayName"`
SupportedVideoCodecs []string `json:"supportedVideoCodecs"`
}
// GetAvailableVideoEncoders probes ffmpeg to discover which encoder+codec combinations
// are available on this system. Returns only encoders that have at least one
// supported codec available.
func GetAvailableVideoEncoders(ffmpegPath string) []EncoderCodecSupport {
ffmpegEncoders := getFFmpegEncoders(ffmpegPath)
// Sort encoder names for deterministic output order.
encoderNames := make([]string, 0, len(encoderRegistry))
for name := range encoderRegistry {
encoderNames = append(encoderNames, name)
}
sort.Strings(encoderNames)
var available []EncoderCodecSupport
for _, name := range encoderNames {
enc := encoderRegistry[name]
var supportedCodecs []string
for _, codecName := range enc.SupportedVideoCodecs() {
ffmpegEncoderName := enc.FFmpegEncoderForCodec(codecName)
if _, ok := ffmpegEncoders[ffmpegEncoderName]; ok {
supportedCodecs = append(supportedCodecs, codecName)
}
}
if len(supportedCodecs) > 0 {
available = append(available, EncoderCodecSupport{
EncoderType: enc.Name(),
EncoderDisplayName: enc.DisplayName(),
SupportedVideoCodecs: supportedCodecs,
})
}
}
// Always include software encoder even if ffmpeg probe fails,
// as libx264 is almost universally available.
hasSoftware := false
for _, enc := range available {
if enc.EncoderType == "software" {
hasSoftware = true
break
}
}
if !hasSoftware {
available = append([]EncoderCodecSupport{{
EncoderType: "software",
EncoderDisplayName: "Software (CPU)",
SupportedVideoCodecs: []string{"h264"},
}}, available...)
}
return available
}
// GetAvailableAudioCodecs probes ffmpeg to discover which audio codecs are available.
func GetAvailableAudioCodecs(ffmpegPath string) []string {
ffmpegEncoders := getFFmpegEncoders(ffmpegPath)
var available []string
for _, codec := range audioCodecRegistry {
if _, ok := ffmpegEncoders[codec.FFmpegEncoderName()]; ok {
available = append(available, codec.Name())
}
}
// Always include AAC as it is built into ffmpeg.
hasAAC := false
for _, name := range available {
if name == "aac" {
hasAAC = true
break
}
}
if !hasAAC {
available = append([]string{"aac"}, available...)
}
sort.Strings(available)
return available
}
// getFFmpegEncoders runs `ffmpeg -encoders` and returns a set of encoder names.
func getFFmpegEncoders(ffmpegPath string) map[string]struct{} {
result := make(map[string]struct{})
cmd := exec.Command(ffmpegPath, "-encoders")
out, err := cmd.CombinedOutput()
if err != nil {
log.Errorln(err)
return result
}
lines := strings.Split(string(out), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if len(line) == 0 || line[0] == '-' || line[0] == '=' {
continue
}
fields := strings.Fields(line)
if len(fields) >= 2 {
result[fields[1]] = struct{}{}
}
}
return result
}
+128
View File
@@ -0,0 +1,128 @@
package transcoder
import (
"fmt"
log "github.com/sirupsen/logrus"
)
const (
pixelFormatYUV420P = "yuv420p"
ffmpegEncoderX264 = "libx264"
)
// CodecProfile holds codec-dependent encoding parameters for an encoder.
type CodecProfile struct {
PixelFormat string
Scaler string
ExtraFilters string
}
// Encoder represents a hardware/software video encoding backend (e.g., software CPU, NVENC, VA-API).
type Encoder interface {
Name() string
DisplayName() string
GlobalFlags() []string
ExtraArguments() []string
ProfileForCodec(codec string) CodecProfile
VariantFlags(v *HLSVariant) []string
SupportedVideoCodecs() []string
FFmpegEncoderForCodec(codec string) string
GetPresetForLevel(l int) string
}
// SoftwareEncoder uses CPU-based encoding via libx264/libx265.
type SoftwareEncoder struct{}
func (e *SoftwareEncoder) Name() string {
return "software"
}
func (e *SoftwareEncoder) DisplayName() string {
return "Software (CPU)"
}
func (e *SoftwareEncoder) GlobalFlags() []string {
return nil
}
func (e *SoftwareEncoder) ExtraArguments() []string {
return []string{"-tune", "zerolatency"}
}
func (e *SoftwareEncoder) ProfileForCodec(codec string) CodecProfile {
return CodecProfile{
PixelFormat: pixelFormatYUV420P,
}
}
func (e *SoftwareEncoder) VariantFlags(v *HLSVariant) []string {
codec := v.videoCodec.Name()
var flags []string
if codec == videoCodecH264 {
flags = append(flags,
fmt.Sprintf("-x264-params:v:%d", v.index), "scenecut=0:open_gop=0",
)
}
flags = append(flags,
fmt.Sprintf("-bufsize:v:%d", v.index), fmt.Sprintf("%dk", v.getBufferSize()),
)
if codec == videoCodecH264 {
flags = append(flags,
fmt.Sprintf("-profile:v:%d", v.index), "high",
)
}
return flags
}
func (e *SoftwareEncoder) SupportedVideoCodecs() []string {
return []string{videoCodecH264}
}
func (e *SoftwareEncoder) FFmpegEncoderForCodec(codec string) string {
switch codec {
case videoCodecH264:
return ffmpegEncoderX264
default:
log.Warnf("software encoder does not support codec %q, falling back to x264", codec)
return ffmpegEncoderX264
}
}
func (e *SoftwareEncoder) GetPresetForLevel(l int) string {
presetMapping := map[int]string{
0: "ultrafast",
1: "superfast",
2: "veryfast",
3: "faster",
4: "fast",
}
preset, ok := presetMapping[l]
if !ok {
defaultPreset := presetMapping[1]
log.Errorf("Invalid level for software preset %d, defaulting to %s", l, defaultPreset)
return defaultPreset
}
return preset
}
var encoderRegistry = map[string]Encoder{
"software": &SoftwareEncoder{},
"nvenc": &NvencEncoder{},
"vaapi": &VaapiEncoder{},
"qsv": &QuicksyncEncoder{},
"omx": &OmxEncoder{},
"v4l2m2m": &V4l2m2mEncoder{},
"videotoolbox": &VideoToolboxEncoder{},
}
func getEncoder(encoderType string) Encoder {
if enc, ok := encoderRegistry[encoderType]; ok {
return enc
}
return &SoftwareEncoder{}
}
+71
View File
@@ -0,0 +1,71 @@
package transcoder
import (
"fmt"
log "github.com/sirupsen/logrus"
)
// NvencEncoder uses NVIDIA GPU hardware encoding.
type NvencEncoder struct{}
func (e *NvencEncoder) Name() string {
return "nvenc"
}
func (e *NvencEncoder) DisplayName() string {
return "NVIDIA NVENC"
}
func (e *NvencEncoder) GlobalFlags() []string {
return []string{
"-hwaccel", "cuda",
}
}
func (e *NvencEncoder) ExtraArguments() []string {
return nil
}
func (e *NvencEncoder) ProfileForCodec(codec string) CodecProfile {
return CodecProfile{
PixelFormat: pixelFormatYUV420P,
}
}
func (e *NvencEncoder) VariantFlags(v *HLSVariant) []string {
return []string{
fmt.Sprintf("-tune:v:%d", v.index), "ll",
}
}
func (e *NvencEncoder) SupportedVideoCodecs() []string {
return []string{videoCodecH264}
}
func (e *NvencEncoder) FFmpegEncoderForCodec(codec string) string {
switch codec {
case videoCodecH264:
return "h264_nvenc"
default:
log.Warnf("nvenc encoder does not support codec %q, falling back to h264_nvenc", codec)
return "h264_nvenc"
}
}
func (e *NvencEncoder) GetPresetForLevel(l int) string {
presetMapping := map[int]string{
0: "p1",
1: "p2",
2: "p3",
3: "p4",
4: "p5",
}
preset, ok := presetMapping[l]
if !ok {
defaultPreset := presetMapping[2]
log.Errorf("Invalid level for nvenc preset %d, defaulting to %s", l, defaultPreset)
return defaultPreset
}
return preset
}
+63
View File
@@ -0,0 +1,63 @@
package transcoder
import log "github.com/sirupsen/logrus"
// OmxEncoder uses OpenMAX hardware encoding (Raspberry Pi / ARM).
type OmxEncoder struct{}
func (e *OmxEncoder) Name() string {
return "omx"
}
func (e *OmxEncoder) DisplayName() string {
return "OpenMAX (omx)"
}
func (e *OmxEncoder) GlobalFlags() []string {
return nil
}
func (e *OmxEncoder) ExtraArguments() []string {
return []string{"-tune", "zerolatency"}
}
func (e *OmxEncoder) ProfileForCodec(codec string) CodecProfile {
return CodecProfile{
PixelFormat: pixelFormatYUV420P,
}
}
func (e *OmxEncoder) VariantFlags(v *HLSVariant) []string {
return nil
}
func (e *OmxEncoder) SupportedVideoCodecs() []string {
return []string{videoCodecH264}
}
func (e *OmxEncoder) FFmpegEncoderForCodec(codec string) string {
switch codec {
case videoCodecH264:
return "h264_omx"
default:
log.Warnf("omx encoder does not support codec %q, falling back to h264_omx", codec)
return "h264_omx"
}
}
func (e *OmxEncoder) GetPresetForLevel(l int) string {
presetMapping := map[int]string{
0: "ultrafast",
1: "superfast",
2: "veryfast",
3: "faster",
4: "fast",
}
preset, ok := presetMapping[l]
if !ok {
defaultPreset := presetMapping[1]
log.Errorf("Invalid level for omx preset %d, defaulting to %s", l, defaultPreset)
return defaultPreset
}
return preset
}
+68
View File
@@ -0,0 +1,68 @@
package transcoder
import log "github.com/sirupsen/logrus"
// QuicksyncEncoder uses Intel Quick Sync Video hardware encoding.
type QuicksyncEncoder struct{}
func (e *QuicksyncEncoder) Name() string {
return "qsv"
}
func (e *QuicksyncEncoder) DisplayName() string {
return "Intel QuickSync"
}
func (e *QuicksyncEncoder) GlobalFlags() []string {
return []string{
"-init_hw_device", "qsv=hw",
"-filter_hw_device", "hw",
}
}
func (e *QuicksyncEncoder) ExtraArguments() []string {
return nil
}
func (e *QuicksyncEncoder) ProfileForCodec(codec string) CodecProfile {
return CodecProfile{
PixelFormat: "qsv",
Scaler: "scale_qsv",
ExtraFilters: "hwupload=extra_hw_frames=64,format=qsv",
}
}
func (e *QuicksyncEncoder) VariantFlags(v *HLSVariant) []string {
return nil
}
func (e *QuicksyncEncoder) SupportedVideoCodecs() []string {
return []string{videoCodecH264}
}
func (e *QuicksyncEncoder) FFmpegEncoderForCodec(codec string) string {
switch codec {
case videoCodecH264:
return "h264_qsv"
default:
log.Warnf("quicksync encoder does not support codec %q, falling back to h264_qsv", codec)
return "h264_qsv"
}
}
func (e *QuicksyncEncoder) GetPresetForLevel(l int) string {
presetMapping := map[int]string{
0: "veryfast",
1: "fast",
2: "medium",
3: "slow",
4: "veryslow",
}
preset, ok := presetMapping[l]
if !ok {
defaultPreset := presetMapping[2]
log.Errorf("Invalid level for quicksync preset %d, defaulting to %s", l, defaultPreset)
return defaultPreset
}
return preset
}
+63
View File
@@ -0,0 +1,63 @@
package transcoder
import log "github.com/sirupsen/logrus"
// V4l2m2mEncoder uses Video4Linux memory-to-memory hardware encoding.
type V4l2m2mEncoder struct{}
func (e *V4l2m2mEncoder) Name() string {
return "v4l2m2m"
}
func (e *V4l2m2mEncoder) DisplayName() string {
return "Video4Linux"
}
func (e *V4l2m2mEncoder) GlobalFlags() []string {
return nil
}
func (e *V4l2m2mEncoder) ExtraArguments() []string {
return nil
}
func (e *V4l2m2mEncoder) ProfileForCodec(codec string) CodecProfile {
return CodecProfile{
PixelFormat: "nv21",
}
}
func (e *V4l2m2mEncoder) VariantFlags(v *HLSVariant) []string {
return nil
}
func (e *V4l2m2mEncoder) SupportedVideoCodecs() []string {
return []string{videoCodecH264}
}
func (e *V4l2m2mEncoder) FFmpegEncoderForCodec(codec string) string {
switch codec {
case videoCodecH264:
return "h264_v4l2m2m"
default:
log.Warnf("v4l2m2m encoder does not support codec %q, falling back to h264_v4l2m2m", codec)
return "h264_v4l2m2m"
}
}
func (e *V4l2m2mEncoder) GetPresetForLevel(l int) string {
presetMapping := map[int]string{
0: "ultrafast",
1: "superfast",
2: "veryfast",
3: "faster",
4: "fast",
}
preset, ok := presetMapping[l]
if !ok {
defaultPreset := presetMapping[1]
log.Errorf("Invalid level for v4l preset %d, defaulting to %s", l, defaultPreset)
return defaultPreset
}
return preset
}
+69
View File
@@ -0,0 +1,69 @@
package transcoder
import log "github.com/sirupsen/logrus"
// VaapiEncoder uses VA-API hardware encoding (Intel/AMD GPUs).
type VaapiEncoder struct{}
func (e *VaapiEncoder) Name() string {
return "vaapi"
}
func (e *VaapiEncoder) DisplayName() string {
return "VA-API"
}
func (e *VaapiEncoder) GlobalFlags() []string {
return []string{
"-hwaccel", "vaapi",
"-hwaccel_output_format", "vaapi",
"-vaapi_device", "/dev/dri/renderD128",
}
}
func (e *VaapiEncoder) ExtraArguments() []string {
return nil
}
func (e *VaapiEncoder) ProfileForCodec(codec string) CodecProfile {
return CodecProfile{
PixelFormat: "vaapi",
Scaler: "scale_vaapi",
ExtraFilters: "hwupload=extra_hw_frames=64,format=vaapi",
}
}
func (e *VaapiEncoder) VariantFlags(v *HLSVariant) []string {
return nil
}
func (e *VaapiEncoder) SupportedVideoCodecs() []string {
return []string{videoCodecH264}
}
func (e *VaapiEncoder) FFmpegEncoderForCodec(codec string) string {
switch codec {
case videoCodecH264:
return "h264_vaapi"
default:
log.Warnf("vaapi encoder does not support codec %q, falling back to h264_vaapi", codec)
return "h264_vaapi"
}
}
func (e *VaapiEncoder) GetPresetForLevel(l int) string {
presetMapping := map[int]string{
0: "ultrafast",
1: "superfast",
2: "veryfast",
3: "faster",
4: "fast",
}
preset, ok := presetMapping[l]
if !ok {
defaultPreset := presetMapping[1]
log.Errorf("Invalid level for vaapi preset %d, defaulting to %s", l, defaultPreset)
return defaultPreset
}
return preset
}
+66
View File
@@ -0,0 +1,66 @@
package transcoder
import log "github.com/sirupsen/logrus"
// VideoToolboxEncoder uses Apple VideoToolbox hardware encoding (macOS).
type VideoToolboxEncoder struct{}
func (e *VideoToolboxEncoder) Name() string {
return "videotoolbox"
}
func (e *VideoToolboxEncoder) DisplayName() string {
return "VideoToolbox"
}
func (e *VideoToolboxEncoder) GlobalFlags() []string {
return nil
}
func (e *VideoToolboxEncoder) ExtraArguments() []string {
return nil
}
func (e *VideoToolboxEncoder) ProfileForCodec(codec string) CodecProfile {
return CodecProfile{
PixelFormat: "nv12",
}
}
func (e *VideoToolboxEncoder) VariantFlags(v *HLSVariant) []string {
if v.cpuUsageLevel >= 3 {
return nil
}
return []string{"-realtime", "true"}
}
func (e *VideoToolboxEncoder) SupportedVideoCodecs() []string {
return []string{videoCodecH264}
}
func (e *VideoToolboxEncoder) FFmpegEncoderForCodec(codec string) string {
switch codec {
case videoCodecH264:
return "h264_videotoolbox"
default:
log.Warnf("videotoolbox encoder does not support codec %q, falling back to h264_videotoolbox", codec)
return "h264_videotoolbox"
}
}
func (e *VideoToolboxEncoder) GetPresetForLevel(l int) string {
presetMapping := map[int]string{
0: "ultrafast",
1: "superfast",
2: "veryfast",
3: "faster",
4: "fast",
}
preset, ok := presetMapping[l]
if !ok {
defaultPreset := presetMapping[1]
log.Errorf("Invalid level for videotoolbox preset %d, defaulting to %s", l, defaultPreset)
return defaultPreset
}
return preset
}
+1 -4
View File
@@ -18,7 +18,6 @@ type FileWriterReceiverServiceCallback interface {
SegmentWritten(localFilePath string)
VariantPlaylistWritten(localFilePath string)
MasterPlaylistWritten(localFilePath string)
InitSegmentWritten(localFilePath string)
}
// FileWriterReceiverService accepts transcoder responses via HTTP and fires the callbacks.
@@ -82,10 +81,8 @@ func (s *FileWriterReceiverService) uploadHandler(w http.ResponseWriter, r *http
func (s *FileWriterReceiverService) fileWritten(path string) {
if utils.GetRelativePathFromAbsolutePath(path) == "hls/stream.m3u8" {
s.callbacks.MasterPlaylistWritten(path)
} else if strings.HasSuffix(path, ".m4s") {
} else if strings.HasSuffix(path, ".ts") {
s.callbacks.SegmentWritten(path)
} else if strings.HasSuffix(path, "init.mp4") {
s.callbacks.InitSegmentWritten(path)
} else if strings.HasSuffix(path, ".m3u8") {
s.callbacks.VariantPlaylistWritten(path)
}
-5
View File
@@ -23,8 +23,3 @@ func (h *HLSHandler) VariantPlaylistWritten(localFilePath string) {
func (h *HLSHandler) MasterPlaylistWritten(localFilePath string) {
h.Storage.MasterPlaylistWritten(localFilePath)
}
// InitSegmentWritten is fired when an fMP4 init segment is written to disk.
func (h *HLSHandler) InitSegmentWritten(localFilePath string) {
h.Storage.SegmentWritten(localFilePath)
}
+3 -18
View File
@@ -1,7 +1,6 @@
package transcoder
import (
"fmt"
"os"
"os/exec"
"path"
@@ -65,7 +64,7 @@ func fireThumbnailGenerator(segmentPath string, variantIndex int) error {
var modTime time.Time
var names []string
for _, f := range files {
if path.Ext(f.Name()) != ".m4s" {
if path.Ext(f.Name()) != ".ts" {
continue
}
@@ -89,21 +88,7 @@ func fireThumbnailGenerator(segmentPath string, variantIndex int) error {
return nil
}
configRepository := configrepository.Get()
// With multiple variants ffmpeg names init segments init_0.mp4, init_1.mp4, etc.
// With a single variant it uses init.mp4.
initSegment := path.Join(framePath, fmt.Sprintf("init_%d.mp4", variantIndex))
if !utils.DoesFileExists(initSegment) {
initSegment = path.Join(framePath, "init.mp4")
}
mostRecentFile := path.Join(framePath, names[0])
// The segment may have been cleaned up between ReadDir and now.
if !utils.DoesFileExists(mostRecentFile) {
return nil
}
// fMP4 segments require the init segment to be readable by ffmpeg
inputArg := fmt.Sprintf("concat:%s|%s", initSegment, mostRecentFile)
ffmpegPath := utils.ValidatedFfmpegPath(configRepository.GetFfMpegPath())
outputFileTemp := path.Join(config.TempDir, "tempthumbnail.jpg")
@@ -111,7 +96,7 @@ func fireThumbnailGenerator(segmentPath string, variantIndex int) error {
"-y", // Overwrite file
"-threads", "1", // Low priority processing
"-t", "1", // Pull from frame 1
"-i", inputArg, // Input (init segment + media segment)
"-i", mostRecentFile, // Input
"-f", "image2", // format
"-vframes", "1", // Single frame
outputFileTemp,
@@ -126,7 +111,7 @@ func fireThumbnailGenerator(segmentPath string, variantIndex int) error {
log.Errorln(err)
}
makeAnimatedGifPreview(inputArg, previewGifFile)
makeAnimatedGifPreview(mostRecentFile, previewGifFile)
return nil
}
+42 -26
View File
@@ -33,7 +33,7 @@ func (e *execInfo) String() string {
// Transcoder is a single instance of a video transcoder.
type Transcoder struct {
codec Codec
encoder Encoder
stdin *io.PipeReader
@@ -66,6 +66,9 @@ type HLSVariant struct {
isVideoPassthrough bool // Override all settings and just copy the video stream
isAudioPassthrough bool // Override all settings and just copy the audio stream
videoCodec VideoCodec
audioCodec AudioCodec
}
// VideoSize is the scaled size of the video output.
@@ -127,7 +130,7 @@ func (t *Transcoder) Start(shouldLog bool) {
flags := t.getFlags()
if shouldLog {
log.Infof("Processing video using codec %s with %d output qualities configured.", t.codec.DisplayName(), len(t.variants))
log.Infof("Processing video using encoder %s with %d output qualities configured.", t.encoder.DisplayName(), len(t.variants))
}
createVariantDirectories()
command := flags.String()
@@ -167,7 +170,7 @@ func (t *Transcoder) Start(shouldLog bool) {
}
if err != nil {
log.Errorln("transcoding error. look at", logging.GetTranscoderLogFilePath(), "to help debug. your copy of ffmpeg may not support your selected codec of", t.codec.Name(), "https://owncast.online/docs/codecs/")
log.Errorln("transcoding error. look at", logging.GetTranscoderLogFilePath(), "to help debug. your copy of ffmpeg may not support your selected encoder of", t.encoder.Name(), "https://owncast.online/docs/codecs/")
}
}
@@ -227,7 +230,7 @@ func (t *Transcoder) getFlags() *execInfo {
}
ffmpegFlags := make([]string, 0, 64)
ffmpegFlags = append(ffmpegFlags, "-hide_banner", "-loglevel", "warning")
ffmpegFlags = append(ffmpegFlags, t.codec.GlobalFlags()...)
ffmpegFlags = append(ffmpegFlags, t.encoder.GlobalFlags()...)
ffmpegFlags = append(ffmpegFlags, []string{
"-fflags", "+genpts", // Generate presentation time stamp if missing
"-flags", "+cgop", // Force closed GOPs
@@ -245,21 +248,18 @@ func (t *Transcoder) getFlags() *execInfo {
ffmpegFlags = append(ffmpegFlags, hlsOptionsString...)
ffmpegFlags = append(ffmpegFlags, hlsEventString...)
ffmpegFlags = append(ffmpegFlags, []string{
"-hls_segment_type", "fmp4",
"-hls_fmp4_init_filename", "init.mp4",
"-bsf:a", "aac_adtstoasc", // Convert AAC ADTS headers to ASC for fMP4 container compatibility
"-segment_format_options", "mpegts_flags=mpegts_copyts=1",
}...)
ffmpegFlags = append(ffmpegFlags, t.codec.ExtraArguments()...)
ffmpegFlags = append(ffmpegFlags, t.encoder.ExtraArguments()...)
ffmpegFlags = append(ffmpegFlags, []string{
// Video settings
"-pix_fmt", t.codec.PixelFormat(),
"-sc_threshold", "0", // Disable scene change detection for creating segments
// Filenames
"-master_pl_name", "stream.m3u8",
"-hls_segment_filename", localListenerAddress + "/%v/stream-" + t.segmentIdentifier + "-%d.m4s", // Send HLS segments back to us over HTTP
"-hls_segment_filename", localListenerAddress + "/%v/stream-" + t.segmentIdentifier + "-%d.ts", // Send HLS segments back to us over HTTP
"-max_muxing_queue_size", "400", // Workaround for Too many packets error: https://trac.ffmpeg.org/ticket/6375?cversion=0
"-method", "PUT", // HLS results sent back to us will be over PUTs
@@ -280,6 +280,9 @@ func getVariantFromConfigQuality(quality models.StreamOutputVariant, index int)
variant.isAudioPassthrough = quality.IsAudioPassthrough
variant.isVideoPassthrough = quality.IsVideoPassthrough
variant.videoCodec = getVideoCodec(quality.VideoCodec)
variant.audioCodec = getAudioCodec(quality.AudioCodec)
// If no audio bitrate is specified then we pass through original audio
if quality.AudioBitrate == 0 {
variant.isAudioPassthrough = true
@@ -320,7 +323,7 @@ func NewTranscoder() *Transcoder {
transcoder.currentStreamOutputSettings = configRepository.GetStreamOutputVariants()
transcoder.currentLatencyLevel = configRepository.GetStreamLatencyLevel()
transcoder.codec = getCodec(configRepository.GetVideoCodec())
transcoder.encoder = getEncoder(configRepository.GetVideoEncoder())
transcoder.segmentOutputPath = config.HLSStoragePath
transcoder.playlistOutputPath = config.HLSStoragePath
@@ -339,26 +342,28 @@ func (v *HLSVariant) getVariantString(t *Transcoder) []string {
variantEncoderCommands := v.getVideoQualityString(t)
variantEncoderCommands = append(variantEncoderCommands, v.getAudioQualityString()...)
profile := t.encoder.ProfileForCodec(v.videoCodec.Name())
if (v.videoSize.Width != 0 || v.videoSize.Height != 0) && !v.isVideoPassthrough {
// Order here matters, you must scale before changing hardware formats
filters := []string{
v.getScalingString(t.codec.Scaler()),
v.getScalingString(profile.Scaler),
}
if t.codec.ExtraFilters() != "" {
filters = append(filters, t.codec.ExtraFilters())
if profile.ExtraFilters != "" {
filters = append(filters, profile.ExtraFilters)
}
scalingAlgorithm := "bilinear"
variantEncoderCommands = append(variantEncoderCommands, []string{
"-sws_flags", scalingAlgorithm,
"-filter:v:" + strconv.Itoa(v.index), strings.Join(filters, ","),
}...)
} else if t.codec.ExtraFilters() != "" && !v.isVideoPassthrough {
} else if profile.ExtraFilters != "" && !v.isVideoPassthrough {
variantEncoderCommands = append(variantEncoderCommands, []string{
"-filter:v:" + strconv.Itoa(v.index), t.codec.ExtraFilters(),
"-filter:v:" + strconv.Itoa(v.index), profile.ExtraFilters,
}...)
}
preset := t.codec.GetPresetForLevel(v.cpuUsageLevel)
preset := t.encoder.GetPresetForLevel(v.cpuUsageLevel)
if preset != "" {
variantEncoderCommands = append(variantEncoderCommands, []string{
"-preset", preset,
@@ -417,18 +422,25 @@ func (v *HLSVariant) getVideoQualityString(t *Transcoder) []string {
}
}
codec := v.videoCodec.Name()
ffmpegEncoder := t.encoder.FFmpegEncoderForCodec(codec)
profile := t.encoder.ProfileForCodec(codec)
gop := v.framerate * t.currentLatencyLevel.SecondsPerSegment // force an i-frame every segment
cmd := make([]string, 0, 20)
cmd = append(cmd,
"-map", "v:0",
fmt.Sprintf("-c:v:%d", v.index), t.codec.Name(), // Video codec used for this variant
fmt.Sprintf("-c:v:%d", v.index), ffmpegEncoder, // Video encoder used for this variant
fmt.Sprintf("-pix_fmt:v:%d", v.index), profile.PixelFormat, // Pixel format for this variant
fmt.Sprintf("-b:v:%d", v.index), fmt.Sprintf("%dk", v.getAllocatedVideoBitrate()), // The average bitrate for this variant allowing space for audio
fmt.Sprintf("-maxrate:v:%d", v.index), fmt.Sprintf("%dk", v.getMaxVideoBitrate()), // The max bitrate allowed for this variant
fmt.Sprintf("-g:v:%d", v.index), fmt.Sprintf("%d", gop), // Suggested interval where i-frames are encoded into the segments
fmt.Sprintf("-keyint_min:v:%d", v.index), fmt.Sprintf("%d", gop), // minimum i-keyframe interval
fmt.Sprintf("-r:v:%d", v.index), fmt.Sprintf("%d", v.framerate),
)
cmd = append(cmd, t.codec.VariantFlags(v)...)
// Add encoder-specific variant flags (e.g., x264-params for software, NVENC low-latency tuning).
cmd = append(cmd, t.encoder.VariantFlags(v)...)
return cmd
}
@@ -457,13 +469,17 @@ func (v *HLSVariant) getAudioQualityString() []string {
}
}
// libfdk_aac is not a part of every ffmpeg install, so use "aac" instead
encoderCodec := "aac"
return []string{
flags := []string{
"-map", "a:0?",
fmt.Sprintf("-c:a:%d", v.index), encoderCodec,
fmt.Sprintf("-c:a:%d", v.index), v.audioCodec.FFmpegEncoderName(),
fmt.Sprintf("-b:a:%d", v.index), v.audioBitrate,
}
for _, bsf := range v.audioCodec.BitstreamFilters() {
flags = append(flags, fmt.Sprintf("-bsf:a:%d", v.index), bsf)
}
return flags
}
// AddVariant adds a new HLS variant to include in the output.
@@ -497,7 +513,7 @@ func (t *Transcoder) SetInternalHTTPPort(port string) {
t.internalListenerPort = port
}
// SetCodec will set the codec to be used for the transocder.
func (t *Transcoder) SetCodec(codecName string) {
t.codec = getCodec(codecName)
// SetEncoder will set the encoder to be used for the transcoder.
func (t *Transcoder) SetEncoder(encoderType string) {
t.encoder = getEncoder(encoderType)
}
+8 -3
View File
@@ -9,7 +9,6 @@ import (
func TestFFmpegNvencCommand(t *testing.T) {
latencyLevel := models.GetLatencyLevel(2)
codec := NvencCodec{}
transcoder := new(Transcoder)
transcoder.ffmpegPath = filepath.Join("fake", "path", "ffmpeg")
@@ -17,12 +16,14 @@ func TestFFmpegNvencCommand(t *testing.T) {
transcoder.SetOutputPath("fakeOutput")
transcoder.SetIdentifier("jdoieGg")
transcoder.SetInternalHTTPPort("8123")
transcoder.SetCodec(codec.Name())
transcoder.SetEncoder("nvenc")
transcoder.currentLatencyLevel = latencyLevel
variant := HLSVariant{}
variant.videoBitrate = 1200
variant.isAudioPassthrough = true
variant.videoCodec = &H264Codec{}
variant.audioCodec = &AACCodec{}
variant.SetVideoFramerate(30)
variant.SetCPUUsageLevel(2)
transcoder.AddVariant(variant)
@@ -30,6 +31,8 @@ func TestFFmpegNvencCommand(t *testing.T) {
variant2 := HLSVariant{}
variant2.videoBitrate = 3500
variant2.isAudioPassthrough = true
variant2.videoCodec = &H264Codec{}
variant2.audioCodec = &AACCodec{}
variant2.SetVideoFramerate(24)
variant2.SetCPUUsageLevel(4)
transcoder.AddVariant(variant2)
@@ -37,12 +40,14 @@ func TestFFmpegNvencCommand(t *testing.T) {
variant3 := HLSVariant{}
variant3.isAudioPassthrough = true
variant3.isVideoPassthrough = true
variant3.videoCodec = &H264Codec{}
variant3.audioCodec = &AACCodec{}
transcoder.AddVariant(variant3)
cmd := transcoder.GetString()
expectedLogPath := "data/logs/transcoder.log"
expected := `FFREPORT=file=` + expectedLogPath + `:level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -hwaccel cuda -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_nvenc -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -tune:v:0 ll -map a:0? -c:a:0 copy -preset p3 -map v:0 -c:v:1 h264_nvenc -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -tune:v:1 ll -map a:0? -c:a:1 copy -preset p5 -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset p1 -var_stream_map v:0,a:0 v:1,a:1 v:2,a:2 -f hls -hls_time 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -hls_segment_type fmp4 -hls_fmp4_init_filename init.mp4 -bsf:a aac_adtstoasc -pix_fmt yuv420p -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdoieGg-%d.m4s -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8`
expected := `FFREPORT=file=` + expectedLogPath + `:level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -hwaccel cuda -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_nvenc -pix_fmt:v:0 yuv420p -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -tune:v:0 ll -map a:0? -c:a:0 copy -preset p3 -map v:0 -c:v:1 h264_nvenc -pix_fmt:v:1 yuv420p -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -tune:v:1 ll -map a:0? -c:a:1 copy -preset p5 -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset p1 -var_stream_map v:0,a:0 v:1,a:1 v:2,a:2 -f hls -hls_time 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdoieGg-%d.ts -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8`
if cmd != expected {
t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected)
+8 -3
View File
@@ -9,7 +9,6 @@ import (
func TestFFmpegOmxCommand(t *testing.T) {
latencyLevel := models.GetLatencyLevel(2)
codec := OmxCodec{}
transcoder := new(Transcoder)
transcoder.ffmpegPath = filepath.Join("fake", "path", "ffmpeg")
@@ -17,12 +16,14 @@ func TestFFmpegOmxCommand(t *testing.T) {
transcoder.SetOutputPath("fakeOutput")
transcoder.SetIdentifier("jdFsdfzGg")
transcoder.SetInternalHTTPPort("8123")
transcoder.SetCodec(codec.Name())
transcoder.SetEncoder("omx")
transcoder.currentLatencyLevel = latencyLevel
variant := HLSVariant{}
variant.videoBitrate = 1200
variant.isAudioPassthrough = true
variant.videoCodec = &H264Codec{}
variant.audioCodec = &AACCodec{}
variant.SetVideoFramerate(30)
variant.SetCPUUsageLevel(2)
transcoder.AddVariant(variant)
@@ -30,6 +31,8 @@ func TestFFmpegOmxCommand(t *testing.T) {
variant2 := HLSVariant{}
variant2.videoBitrate = 3500
variant2.isAudioPassthrough = true
variant2.videoCodec = &H264Codec{}
variant2.audioCodec = &AACCodec{}
variant2.SetVideoFramerate(24)
variant2.SetCPUUsageLevel(4)
transcoder.AddVariant(variant2)
@@ -37,12 +40,14 @@ func TestFFmpegOmxCommand(t *testing.T) {
variant3 := HLSVariant{}
variant3.isAudioPassthrough = true
variant3.isVideoPassthrough = true
variant3.videoCodec = &H264Codec{}
variant3.audioCodec = &AACCodec{}
transcoder.AddVariant(variant3)
cmd := transcoder.GetString()
expectedLogPath := "data/logs/transcoder.log"
expected := `FFREPORT=file=` + expectedLogPath + `:level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_omx -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -map a:0? -c:a:0 copy -preset veryfast -map v:0 -c:v:1 h264_omx -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -map a:0? -c:a:1 copy -preset fast -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset ultrafast -var_stream_map v:0,a:0 v:1,a:1 v:2,a:2 -f hls -hls_time 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -hls_segment_type fmp4 -hls_fmp4_init_filename init.mp4 -bsf:a aac_adtstoasc -tune zerolatency -pix_fmt yuv420p -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdFsdfzGg-%d.m4s -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8`
expected := `FFREPORT=file=` + expectedLogPath + `:level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_omx -pix_fmt:v:0 yuv420p -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -map a:0? -c:a:0 copy -preset veryfast -map v:0 -c:v:1 h264_omx -pix_fmt:v:1 yuv420p -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -map a:0? -c:a:1 copy -preset fast -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset ultrafast -var_stream_map v:0,a:0 v:1,a:1 v:2,a:2 -f hls -hls_time 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -tune zerolatency -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdFsdfzGg-%d.ts -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8`
if cmd != expected {
t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected)
+8 -3
View File
@@ -9,7 +9,6 @@ import (
func TestFFmpegQuicksyncCommand(t *testing.T) {
latencyLevel := models.GetLatencyLevel(2)
codec := QuicksyncCodec{}
transcoder := new(Transcoder)
transcoder.ffmpegPath = filepath.Join("fake", "path", "ffmpeg")
@@ -17,12 +16,14 @@ func TestFFmpegQuicksyncCommand(t *testing.T) {
transcoder.SetOutputPath("fakeOutput")
transcoder.SetIdentifier("jdofFGg")
transcoder.SetInternalHTTPPort("8123")
transcoder.SetCodec(codec.Name())
transcoder.SetEncoder("qsv")
transcoder.currentLatencyLevel = latencyLevel
variant := HLSVariant{}
variant.videoBitrate = 1200
variant.isAudioPassthrough = true
variant.videoCodec = &H264Codec{}
variant.audioCodec = &AACCodec{}
variant.SetVideoFramerate(30)
variant.SetCPUUsageLevel(2)
transcoder.AddVariant(variant)
@@ -30,6 +31,8 @@ func TestFFmpegQuicksyncCommand(t *testing.T) {
variant2 := HLSVariant{}
variant2.videoBitrate = 3500
variant2.isAudioPassthrough = true
variant2.videoCodec = &H264Codec{}
variant2.audioCodec = &AACCodec{}
variant2.SetVideoFramerate(24)
variant2.SetCPUUsageLevel(4)
transcoder.AddVariant(variant2)
@@ -37,12 +40,14 @@ func TestFFmpegQuicksyncCommand(t *testing.T) {
variant3 := HLSVariant{}
variant3.isAudioPassthrough = true
variant3.isVideoPassthrough = true
variant3.videoCodec = &H264Codec{}
variant3.audioCodec = &AACCodec{}
transcoder.AddVariant(variant3)
cmd := transcoder.GetString()
expectedLogPath := "data/logs/transcoder.log"
expected := `FFREPORT=file=` + expectedLogPath + `:level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -init_hw_device qsv=hw -filter_hw_device hw -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_qsv -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -map a:0? -c:a:0 copy -filter:v:0 hwupload=extra_hw_frames=64,format=qsv -preset medium -map v:0 -c:v:1 h264_qsv -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -map a:0? -c:a:1 copy -filter:v:1 hwupload=extra_hw_frames=64,format=qsv -preset veryslow -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset veryfast -var_stream_map v:0,a:0 v:1,a:1 v:2,a:2 -f hls -hls_time 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -hls_segment_type fmp4 -hls_fmp4_init_filename init.mp4 -bsf:a aac_adtstoasc -pix_fmt qsv -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdofFGg-%d.m4s -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8`
expected := `FFREPORT=file=` + expectedLogPath + `:level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -init_hw_device qsv=hw -filter_hw_device hw -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_qsv -pix_fmt:v:0 qsv -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -map a:0? -c:a:0 copy -filter:v:0 hwupload=extra_hw_frames=64,format=qsv -preset medium -map v:0 -c:v:1 h264_qsv -pix_fmt:v:1 qsv -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -map a:0? -c:a:1 copy -filter:v:1 hwupload=extra_hw_frames=64,format=qsv -preset veryslow -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset veryfast -var_stream_map v:0,a:0 v:1,a:1 v:2,a:2 -f hls -hls_time 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdofFGg-%d.ts -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8`
if cmd != expected {
t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected)
+8 -3
View File
@@ -9,7 +9,6 @@ import (
func TestFFmpegVaapiCommand(t *testing.T) {
latencyLevel := models.GetLatencyLevel(2)
codec := VaapiCodec{}
transcoder := new(Transcoder)
transcoder.ffmpegPath = filepath.Join("fake", "path", "ffmpeg")
@@ -17,12 +16,14 @@ func TestFFmpegVaapiCommand(t *testing.T) {
transcoder.SetOutputPath("fakeOutput")
transcoder.SetIdentifier("jdofFGg")
transcoder.SetInternalHTTPPort("8123")
transcoder.SetCodec(codec.Name())
transcoder.SetEncoder("vaapi")
transcoder.currentLatencyLevel = latencyLevel
variant := HLSVariant{}
variant.videoBitrate = 1200
variant.isAudioPassthrough = true
variant.videoCodec = &H264Codec{}
variant.audioCodec = &AACCodec{}
variant.SetVideoFramerate(30)
variant.SetCPUUsageLevel(2)
transcoder.AddVariant(variant)
@@ -30,6 +31,8 @@ func TestFFmpegVaapiCommand(t *testing.T) {
variant2 := HLSVariant{}
variant2.videoBitrate = 3500
variant2.isAudioPassthrough = true
variant2.videoCodec = &H264Codec{}
variant2.audioCodec = &AACCodec{}
variant2.SetVideoFramerate(24)
variant2.SetCPUUsageLevel(4)
transcoder.AddVariant(variant2)
@@ -37,12 +40,14 @@ func TestFFmpegVaapiCommand(t *testing.T) {
variant3 := HLSVariant{}
variant3.isAudioPassthrough = true
variant3.isVideoPassthrough = true
variant3.videoCodec = &H264Codec{}
variant3.audioCodec = &AACCodec{}
transcoder.AddVariant(variant3)
cmd := transcoder.GetString()
expectedLogPath := "data/logs/transcoder.log"
expected := `FFREPORT=file=` + expectedLogPath + `:level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -hwaccel vaapi -hwaccel_output_format vaapi -vaapi_device /dev/dri/renderD128 -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_vaapi -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -map a:0? -c:a:0 copy -filter:v:0 hwupload=extra_hw_frames=64,format=vaapi -preset veryfast -map v:0 -c:v:1 h264_vaapi -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -map a:0? -c:a:1 copy -filter:v:1 hwupload=extra_hw_frames=64,format=vaapi -preset fast -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset ultrafast -var_stream_map v:0,a:0 v:1,a:1 v:2,a:2 -f hls -hls_time 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -hls_segment_type fmp4 -hls_fmp4_init_filename init.mp4 -bsf:a aac_adtstoasc -pix_fmt vaapi -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdofFGg-%d.m4s -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8`
expected := `FFREPORT=file=` + expectedLogPath + `:level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -hwaccel vaapi -hwaccel_output_format vaapi -vaapi_device /dev/dri/renderD128 -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_vaapi -pix_fmt:v:0 vaapi -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -map a:0? -c:a:0 copy -filter:v:0 hwupload=extra_hw_frames=64,format=vaapi -preset veryfast -map v:0 -c:v:1 h264_vaapi -pix_fmt:v:1 vaapi -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -map a:0? -c:a:1 copy -filter:v:1 hwupload=extra_hw_frames=64,format=vaapi -preset fast -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset ultrafast -var_stream_map v:0,a:0 v:1,a:1 v:2,a:2 -f hls -hls_time 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdofFGg-%d.ts -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8`
if cmd != expected {
t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected)
@@ -9,7 +9,6 @@ import (
func TestFFmpegVideoToolboxCommand(t *testing.T) {
latencyLevel := models.GetLatencyLevel(2)
codec := VideoToolboxCodec{}
transcoder := new(Transcoder)
transcoder.ffmpegPath = filepath.Join("fake", "path", "ffmpeg")
@@ -17,12 +16,14 @@ func TestFFmpegVideoToolboxCommand(t *testing.T) {
transcoder.SetOutputPath("fakeOutput")
transcoder.SetIdentifier("jdFsdfzGg")
transcoder.SetInternalHTTPPort("8123")
transcoder.SetCodec(codec.Name())
transcoder.SetEncoder("videotoolbox")
transcoder.currentLatencyLevel = latencyLevel
variant := HLSVariant{}
variant.videoBitrate = 1200
variant.isAudioPassthrough = true
variant.videoCodec = &H264Codec{}
variant.audioCodec = &AACCodec{}
variant.SetVideoFramerate(30)
variant.SetCPUUsageLevel(2)
transcoder.AddVariant(variant)
@@ -30,6 +31,8 @@ func TestFFmpegVideoToolboxCommand(t *testing.T) {
variant2 := HLSVariant{}
variant2.videoBitrate = 3500
variant2.isAudioPassthrough = true
variant2.videoCodec = &H264Codec{}
variant2.audioCodec = &AACCodec{}
variant2.SetVideoFramerate(24)
variant2.SetCPUUsageLevel(4)
transcoder.AddVariant(variant2)
@@ -37,12 +40,14 @@ func TestFFmpegVideoToolboxCommand(t *testing.T) {
variant3 := HLSVariant{}
variant3.isAudioPassthrough = true
variant3.isVideoPassthrough = true
variant3.videoCodec = &H264Codec{}
variant3.audioCodec = &AACCodec{}
transcoder.AddVariant(variant3)
cmd := transcoder.GetString()
expectedLogPath := "data/logs/transcoder.log"
expected := `FFREPORT=file=` + expectedLogPath + `:level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_videotoolbox -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -realtime true -map a:0? -c:a:0 copy -preset veryfast -map v:0 -c:v:1 h264_videotoolbox -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -map a:0? -c:a:1 copy -preset fast -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset ultrafast -var_stream_map v:0,a:0 v:1,a:1 v:2,a:2 -f hls -hls_time 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -hls_segment_type fmp4 -hls_fmp4_init_filename init.mp4 -bsf:a aac_adtstoasc -pix_fmt nv12 -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdFsdfzGg-%d.m4s -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8`
expected := `FFREPORT=file=` + expectedLogPath + `:level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_videotoolbox -pix_fmt:v:0 nv12 -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -realtime true -map a:0? -c:a:0 copy -preset veryfast -map v:0 -c:v:1 h264_videotoolbox -pix_fmt:v:1 nv12 -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -map a:0? -c:a:1 copy -preset fast -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset ultrafast -var_stream_map v:0,a:0 v:1,a:1 v:2,a:2 -f hls -hls_time 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdFsdfzGg-%d.ts -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8`
if cmd != expected {
t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected)
+8 -3
View File
@@ -9,7 +9,6 @@ import (
func TestFFmpegx264Command(t *testing.T) {
latencyLevel := models.GetLatencyLevel(2)
codec := Libx264Codec{}
transcoder := new(Transcoder)
transcoder.ffmpegPath = filepath.Join("fake", "path", "ffmpeg")
@@ -17,12 +16,14 @@ func TestFFmpegx264Command(t *testing.T) {
transcoder.SetOutputPath("fakeOutput")
transcoder.SetIdentifier("jdofFGg")
transcoder.SetInternalHTTPPort("8123")
transcoder.SetCodec(codec.Name())
transcoder.SetEncoder("software")
transcoder.currentLatencyLevel = latencyLevel
variant := HLSVariant{}
variant.videoBitrate = 1200
variant.isAudioPassthrough = true
variant.videoCodec = &H264Codec{}
variant.audioCodec = &AACCodec{}
variant.SetVideoFramerate(30)
variant.SetCPUUsageLevel(2)
transcoder.AddVariant(variant)
@@ -30,6 +31,8 @@ func TestFFmpegx264Command(t *testing.T) {
variant2 := HLSVariant{}
variant2.videoBitrate = 3500
variant2.isAudioPassthrough = true
variant2.videoCodec = &H264Codec{}
variant2.audioCodec = &AACCodec{}
variant2.SetVideoFramerate(24)
variant2.SetCPUUsageLevel(4)
transcoder.AddVariant(variant2)
@@ -37,12 +40,14 @@ func TestFFmpegx264Command(t *testing.T) {
variant3 := HLSVariant{}
variant3.isAudioPassthrough = true
variant3.isVideoPassthrough = true
variant3.videoCodec = &H264Codec{}
variant3.audioCodec = &AACCodec{}
transcoder.AddVariant(variant3)
cmd := transcoder.GetString()
expectedLogPath := "data/logs/transcoder.log"
expected := `FFREPORT=file=` + expectedLogPath + `:level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 libx264 -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -x264-params:v:0 scenecut=0:open_gop=0 -bufsize:v:0 1088k -profile:v:0 high -map a:0? -c:a:0 copy -preset veryfast -map v:0 -c:v:1 libx264 -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -x264-params:v:1 scenecut=0:open_gop=0 -bufsize:v:1 3572k -profile:v:1 high -map a:0? -c:a:1 copy -preset fast -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset ultrafast -var_stream_map v:0,a:0 v:1,a:1 v:2,a:2 -f hls -hls_time 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -hls_segment_type fmp4 -hls_fmp4_init_filename init.mp4 -bsf:a aac_adtstoasc -tune zerolatency -pix_fmt yuv420p -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdofFGg-%d.m4s -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8`
expected := `FFREPORT=file=` + expectedLogPath + `:level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 libx264 -pix_fmt:v:0 yuv420p -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -x264-params:v:0 scenecut=0:open_gop=0 -bufsize:v:0 1088k -profile:v:0 high -map a:0? -c:a:0 copy -preset veryfast -map v:0 -c:v:1 libx264 -pix_fmt:v:1 yuv420p -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -x264-params:v:1 scenecut=0:open_gop=0 -bufsize:v:1 3572k -profile:v:1 high -map a:0? -c:a:1 copy -preset fast -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset ultrafast -var_stream_map v:0,a:0 v:1,a:1 v:2,a:2 -f hls -hls_time 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -tune zerolatency -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdofFGg-%d.ts -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8`
if cmd != expected {
t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected)
+31
View File
@@ -0,0 +1,31 @@
package transcoder
const videoCodecH264 = "h264"
// VideoCodec represents a video compression format (e.g., H.264, H.265).
type VideoCodec interface {
Name() string
DisplayName() string
}
// H264Codec implements VideoCodec for the H.264 format.
type H264Codec struct{}
func (c *H264Codec) Name() string {
return videoCodecH264
}
func (c *H264Codec) DisplayName() string {
return "H.264"
}
var videoCodecRegistry = map[string]VideoCodec{
videoCodecH264: &H264Codec{},
}
func getVideoCodec(name string) VideoCodec {
if codec, ok := videoCodecRegistry[name]; ok {
return codec
}
return &H264Codec{}
}
+7 -11
View File
@@ -16,25 +16,25 @@ require (
github.com/hashicorp/go-retryablehttp v0.7.8
github.com/jellydator/ttlcache/v3 v3.4.0
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
github.com/mattn/go-sqlite3 v1.14.37
github.com/mattn/go-sqlite3 v1.14.40
github.com/microcosm-cc/bluemonday v1.0.27
github.com/mssola/user_agent v0.6.0
github.com/nakabonne/tstorage v0.3.6
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590
github.com/oapi-codegen/runtime v1.3.0
github.com/oapi-codegen/runtime v1.3.1
github.com/oschwald/geoip2-golang v1.13.0
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.23.2
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5
github.com/schollz/sqlite3dump v1.3.1
github.com/shirou/gopsutil/v4 v4.26.2
github.com/shirou/gopsutil/v4 v4.25.8
github.com/sirupsen/logrus v1.9.4
github.com/stretchr/testify v1.11.1
github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569
github.com/yuin/goldmark v1.7.13
github.com/yuin/goldmark-emoji v1.0.6
golang.org/x/crypto v0.49.0
golang.org/x/mod v0.34.0
golang.org/x/mod v0.33.0
golang.org/x/net v0.51.0
golang.org/x/time v0.15.0
gopkg.in/evanphx/json-patch.v5 v5.9.11
@@ -48,8 +48,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/fatih/color v1.19.0 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/google/uuid v1.6.0 // indirect
@@ -60,7 +59,6 @@ require (
github.com/klauspost/compress v1.18.0 // indirect
github.com/lestrrat-go/strftime v1.1.1 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oschwald/maxminddb-golang v1.13.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -68,16 +66,14 @@ require (
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+25 -18
View File
@@ -24,10 +24,10 @@ github.com/dave/jennifer v1.3.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhr
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-fed/httpsig v0.1.1-0.20190914113940-c2de3672e5b5/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE=
@@ -87,13 +87,19 @@ github.com/lestrrat-go/strftime v1.1.1 h1:zgf8QCsgj27GlKBy3SU9/8MMgegZ8UCzlCyHYr
github.com/lestrrat-go/strftime v1.1.1/go.mod h1:YDrzHJAODYQ+xxvrn5SG01uFIQAeDTzpxNVppCz7Nmw=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg=
github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.38 h1:tDUzL85kMvOrvpCt8P64SbGgVFtJB11GPi2AdmITgb4=
github.com/mattn/go-sqlite3 v1.14.38/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.39 h1:sIwSjlJGOaRJjw44/HXaeTblZMjseqr6OOio1tz/+JI=
github.com/mattn/go-sqlite3 v1.14.39/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.40 h1:f7+saIsbq4EF86mUqe0uiecQOJYMOdfi5uATADmUG94=
github.com/mattn/go-sqlite3 v1.14.40/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/mssola/user_agent v0.6.0 h1:uwPR4rtWlCHRFyyP9u2KOV0u8iQXmS7Z7feTrstQwk4=
@@ -106,6 +112,8 @@ github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590 h1:PnxRU8L8Y2q82vFC2Qd
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590/go.mod h1:XmAOs6UJXpNXRwKk+KY/nv5kL6xXYXyellk+A1pTlko=
github.com/oapi-codegen/runtime v1.3.0 h1:vyK1zc0gDWWXgk2xoQa4+X4RNNc5SL2RbTpJS/4vMYA=
github.com/oapi-codegen/runtime v1.3.0/go.mod h1:kOdeacKy7t40Rclb1je37ZLFboFxh+YLy0zaPCMibPY=
github.com/oapi-codegen/runtime v1.3.1 h1:RgDY6J4OGQLbRXhG/Xpt3vSVqYpHQS7hN4m85+5xB9g=
github.com/oapi-codegen/runtime v1.3.1/go.mod h1:kOdeacKy7t40Rclb1je37ZLFboFxh+YLy0zaPCMibPY=
github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI=
github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
@@ -130,12 +138,12 @@ github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzM
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo=
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a h1:w3tdWGKbLGBPtR/8/oO74W6hmz0qE5q0z9aqSAewaaM=
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY=
github.com/schollz/sqlite3dump v1.3.1 h1:QXizJ7XEJ7hggjqjZ3YRtF3+javm8zKtzNByYtEkPRA=
github.com/schollz/sqlite3dump v1.3.1/go.mod h1:mzSTjZpJH4zAb1FN3iNlhWPbbdyeBpOaTW0hukyMHyI=
github.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cIRGUI=
github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/shirou/gopsutil/v4 v4.25.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970=
github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/spf13/cobra v0.0.4-0.20190109003409-7547e83b2d85/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
@@ -153,10 +161,10 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569 h1:xzABM9let0HLLqFypcxvLmlvEciCHL7+Lv+4vwZqecI=
github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569/go.mod h1:2Ly+NIftZN4de9zRmENdYbvPQeaVIYKWpLFStLFEBgI=
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g=
github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ=
@@ -186,8 +194,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -266,9 +274,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v5 v5.9.11 h1:OMPeiLomOQwe8+Ku4nwXsdOmrRw2vGUpP3XgLj3ojNw=
gopkg.in/evanphx/json-patch.v5 v5.9.11/go.mod h1:/kvTRh1TVm5wuM6OkHxqXtE/1nUZZpihg29RtuIyfvk=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+5
View File
@@ -28,6 +28,11 @@ type StreamOutputVariant struct {
Framerate int `yaml:"framerate" json:"framerate"`
// CPUUsageLevel represents a codec preset to configure CPU usage.
CPUUsageLevel int `json:"cpuUsageLevel"`
// VideoCodec is the video compression format for this variant (e.g., "h264").
VideoCodec string `yaml:"videoCodec" json:"videoCodec,omitempty"`
// AudioCodec is the audio compression format for this variant (e.g., "aac").
AudioCodec string `yaml:"audioCodec" json:"audioCodec,omitempty"`
}
// GetFramerate returns the framerate or default.
+25 -4
View File
@@ -1545,7 +1545,8 @@ paths:
$ref: '#/components/responses/204'
/admin/config/video/codec:
post:
summary: Set video codec
summary: Set video encoder type
description: "Set the global video encoder backend (e.g., software, nvenc, vaapi)."
operationId: SetVideoCodec
tags: ['Internal', 'Admin', 'Video']
security:
@@ -1554,7 +1555,7 @@ paths:
$ref: '#/components/requestBodies/AdminConfigValue'
responses:
'200':
description: Video codec updated
description: Video encoder updated
content:
application/json:
schema:
@@ -3918,6 +3919,12 @@ components:
type: integer
cpuUsageLevel:
type: integer
videoCodec:
type: string
description: "Video compression format (e.g., h264). Defaults to h264."
audioCodec:
type: string
description: "Audio compression format (e.g., aac). Defaults to aac."
LatencyLevel:
type: object
properties:
@@ -4204,15 +4211,29 @@ components:
type: string
webServerIP:
type: string
videoCodec:
videoEncoder:
type: string
description: "The global video encoder type (e.g., software, nvenc, vaapi)"
videoServingEndpoint:
type: string
s3:
$ref: '#/components/schemas/S3Info'
federation:
$ref: '#/components/schemas/AdminFederationConfig'
supportedCodecs:
supportedVideoEncoders:
type: array
items:
type: object
properties:
encoderType:
type: string
encoderDisplayName:
type: string
supportedVideoCodecs:
type: array
items:
type: string
supportedAudioCodecs:
type: array
items:
type: string
@@ -9,7 +9,7 @@ import (
)
const (
datastoreValuesVersion = 4
datastoreValuesVersion = 5
datastoreValueVersionKey = "DATA_STORE_VERSION"
)
@@ -30,6 +30,8 @@ func migrateDatastoreValues(datastore *data.Datastore, configRepository ConfigRe
migrateToDatastoreValues3ServingEndpoint3(configRepository)
case 3:
migrateToDatastoreValues4(datastore, configRepository)
case 4:
migrateToDatastoreValues5(datastore, configRepository)
default:
log.Fatalln("missing datastore values migration step")
}
@@ -86,3 +88,44 @@ func migrateToDatastoreValues4(datastore *data.Datastore, configRepository Confi
log.Fatalln("error migrating admin password:", err)
}
}
func migrateToDatastoreValues5(datastore *data.Datastore, configRepository ConfigRepository) {
// Migrate the old video_codec value (which was really an encoder name like "libx264",
// "h264_nvenc") to the new video_encoder key with the encoder type ("software", "nvenc").
// Inlined from transcoder.OldCodecNameToEncoderType to avoid an import cycle
// (core/transcoder imports persistence/configrepository).
oldCodecToEncoderType := map[string]string{
"libx264": "software",
"h264_nvenc": "nvenc",
"h264_vaapi": "vaapi",
"h264_qsv": "qsv",
"h264_omx": "omx",
"h264_v4l2m2m": "v4l2m2m",
"h264_videotoolbox": "videotoolbox",
}
oldCodecValue, _ := datastore.GetString("video_codec")
if oldCodecValue != "" {
encoderType, ok := oldCodecToEncoderType[oldCodecValue]
if !ok {
encoderType = "software"
}
if err := configRepository.SetVideoEncoder(encoderType); err != nil {
log.Errorln("error migrating video encoder setting:", err)
}
}
// Add default video and audio codec values to existing stream output variants.
variants := configRepository.GetStreamOutputVariants()
for i := range variants {
if variants[i].VideoCodec == "" {
variants[i].VideoCodec = "h264"
}
if variants[i].AudioCodec == "" {
variants[i].AudioCodec = "aac"
}
}
if err := configRepository.SetStreamOutputVariants(variants); err != nil {
log.Errorln("error migrating stream output variants with codec defaults:", err)
}
}
+1 -1
View File
@@ -30,7 +30,7 @@ const (
externalActionsKey = "external_actions"
customStylesKey = "custom_styles"
customJavascriptKey = "custom_javascript"
videoCodecKey = "video_codec"
videoEncoderKey = "video_encoder"
blockedUsernamesKey = "blocked_usernames"
publicKeyKey = "public_key"
privateKeyKey = "private_key"
@@ -75,8 +75,8 @@ type ConfigRepository interface {
GetCustomStyles() string
SetCustomJavascript(styles string) error
GetCustomJavascript() string
SetVideoCodec(codec string) error
GetVideoCodec() string
SetVideoEncoder(encoder string) error
GetVideoEncoder() string
VerifySettings() error
FindHighestVideoQualityIndex(qualities []models.StreamOutputVariant) (int, bool)
GetForbiddenUsernameList() []string
@@ -623,19 +623,19 @@ func (r *SqlConfigRepository) GetCustomJavascript() string {
return style
}
// SetVideoCodec will set the codec used for video encoding.
func (r *SqlConfigRepository) SetVideoCodec(codec string) error {
return r.datastore.SetString(videoCodecKey, codec)
// SetVideoEncoder will set the encoder used for video encoding.
func (r *SqlConfigRepository) SetVideoEncoder(encoder string) error {
return r.datastore.SetString(videoEncoderKey, encoder)
}
// GetVideoCodec returns the codec to use for transcoding video.
func (r *SqlConfigRepository) GetVideoCodec() string {
codec, err := r.datastore.GetString(videoCodecKey)
if codec == "" || err != nil {
return "libx264" // Default value
// GetVideoEncoder returns the encoder to use for transcoding video.
func (r *SqlConfigRepository) GetVideoEncoder() string {
encoder, err := r.datastore.GetString(videoEncoderKey)
if encoder == "" || err != nil {
return "software"
}
return codec
return encoder
}
// VerifySettings will perform a sanity check for specific settings values.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+5 -13
View File
@@ -51,20 +51,12 @@ func GetWebIndexTemplate() (*template.Template, error) {
return tmpl, err
}
//go:embed offline-init.mp4
var offlineInitSegment []byte
//go:embed offline-v2.ts
var offlineVideoSegment []byte
// GetOfflineInitSegment will return the fMP4 init segment for the offline clip.
func GetOfflineInitSegment() []byte {
return getFileSystemStaticFileOrDefault("offline-init.mp4", offlineInitSegment)
}
//go:embed offline-v2.m4s
var offlineMediaSegment []byte
// GetOfflineMediaSegment will return the fMP4 media segment for the offline clip.
func GetOfflineMediaSegment() []byte {
return getFileSystemStaticFileOrDefault("offline-v2.m4s", offlineMediaSegment)
// GetOfflineSegment will return the offline video segment data.
func GetOfflineSegment() []byte {
return getFileSystemStaticFileOrDefault("offline-v2.ts", offlineVideoSegment)
}
//go:embed img/logo.png
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.VideoJS_player__GD36e{width:100%;position:relative;overflow:hidden}@media(hover:hover)and (pointer:fine){.VideoJS_player__GD36e .vjs-control-bar .vjs-control:hover .vjs-control-text{display:block!important;visibility:visible!important;width:auto!important;height:auto!important;clip:auto!important;margin:0!important;position:absolute;bottom:45px;left:50%;transform:translateX(-50%);white-space:nowrap;z-index:1000;padding:5px 10px;background:hsla(0,0%,8%,.9);color:#fff;border-radius:4px;font-size:12px;line-height:1.2;pointer-events:none}.VideoJS_player__GD36e .vjs-control-bar .vjs-control:hover .vjs-control-text:empty,.VideoJS_player__GD36e .vjs-volume-panel:hover .vjs-mute-control:not(:hover) .vjs-control-text{display:none!important}.VideoJS_player__GD36e .vjs-control:first-child:hover .vjs-control-text{left:0;transform:translateX(6px)}.VideoJS_player__GD36e .vjs-control:first-child:hover .vjs-control-text:after{left:15%}.VideoJS_player__GD36e .vjs-control:last-child:hover .vjs-control-text{left:auto;right:0;transform:translateX(-6px)}.VideoJS_player__GD36e .vjs-control:last-child:hover .vjs-control-text:after{left:auto;right:15%;transform:translateX(50%)}}.VideoPoster_poster__6rnLj{display:flex;justify-content:center;width:100%;height:100%}.VideoPoster_image__8kRcw{background-color:#000}.OwncastPlayer_container__CR5Ry{display:grid;width:100%;justify-items:center;height:var(--player-container-height);aspect-ratio:16/9}@media(width <= 1200px){.OwncastPlayer_container__CR5Ry{height:100%;max-height:var(--player-container-height)}}@media only screen and (width <= 768px){.OwncastPlayer_container__CR5Ry{height:var(--player-container-height);max-height:var(--player-container-height)}}.OwncastPlayer_container__CR5Ry .OwncastPlayer_player__dCDjy,.OwncastPlayer_container__CR5Ry .OwncastPlayer_poster__tbpwE{width:100%;grid-column:1;grid-row:1}
-1
View File
@@ -1 +0,0 @@
.VideoJS_player__GD36e{width:100%}.VideoPoster_poster__6rnLj{display:flex;justify-content:center;width:100%;height:100%}.VideoPoster_image__8kRcw{background-color:#000}.OwncastPlayer_container__CR5Ry{display:grid;width:100%;justify-items:center;height:var(--player-container-height);aspect-ratio:16/9}@media(width <= 1200px){.OwncastPlayer_container__CR5Ry{height:100%;max-height:var(--player-container-height)}}@media only screen and (width <= 768px){.OwncastPlayer_container__CR5Ry{height:var(--player-container-height);max-height:var(--player-container-height)}}.OwncastPlayer_container__CR5Ry .OwncastPlayer_player__dCDjy,.OwncastPlayer_container__CR5Ry .OwncastPlayer_poster__tbpwE{width:100%;grid-column:1;grid-row:1}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More