├── .clang-format
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ ├── ci.yml
│ └── docker.yml
├── .gitignore
├── CMakeLists.txt
├── CODE_OF_CONDUCT.md
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── cmake
├── FindLMDB.cmake
└── version.cmake
├── config_example
├── docker
├── nginx.conf
└── supervisord.conf
├── docs
├── .gitignore
├── .vitepress
│ ├── config.mts
│ ├── de.ts
│ ├── en.ts
│ └── theme
│ │ ├── custom.css
│ │ ├── index.mts
│ │ └── tailwind.css
├── changelog.md
├── de
│ ├── changelog.md
│ ├── hosted
│ │ ├── howto
│ │ │ ├── chat.md
│ │ │ ├── login.md
│ │ │ ├── overview.md
│ │ │ └── recording.md
│ │ ├── started.md
│ │ └── what-is-mix-rooms.md
│ ├── index.md
│ └── self-hosting
│ │ ├── install-docker.md
│ │ ├── install-intro.md
│ │ ├── install-source-archlinux.md
│ │ ├── install-source-build.md
│ │ ├── install-source-ubuntu24_04.md
│ │ └── install-source-update.md
├── hosted
│ ├── howto
│ │ ├── chat.md
│ │ ├── login.md
│ │ ├── overview.md
│ │ └── recording.md
│ ├── started.md
│ └── what-is-mix-rooms.md
├── index.md
├── package-lock.json
├── package.json
├── public
│ ├── create_room.png
│ ├── emoticons.mp4
│ ├── login.png
│ ├── room_links.png
│ ├── social_login.mp4
│ ├── solo_button.png
│ └── vidconv.drawio.png
├── self-hosting
│ ├── install-docker.md
│ ├── install-intro.md
│ ├── install-source-archlinux.md
│ ├── install-source-build.md
│ ├── install-source-ubuntu24_04.md
│ └── install-source-update.md
└── vite.config.mjs
├── entrypoint.sh
├── include
└── mix.h
├── modules
├── amix
│ ├── CMakeLists.txt
│ ├── amix.c
│ ├── amix.h
│ ├── flac.c
│ └── record.c
└── vmix
│ ├── CMakeLists.txt
│ ├── codec.c
│ ├── disp.c
│ ├── pktsrc.c
│ ├── record.c
│ ├── src.c
│ ├── vmix.c
│ └── vmix.h
├── patches
├── 2634.patch
├── 2636.patch
├── 2861.patch
├── avcodec_decode_scale_crash.patch
├── avcodec_encode_refs.patch
├── baresip_2936.patch
├── baresip_ice_sdp_mdns.patch
├── baresip_jbuf_nack.patch
├── baresip_packet_dup_handler.patch
├── baresip_stream_enable.patch
├── baresip_video_latency.patch
├── baresip_video_remove_sendq_empty.patch
├── re_864.patch
├── re_877.patch
├── re_aubuf_timestamp_order_fix.patch
└── re_vidmix_clear.patch
├── src
├── avatar.c
├── chat.c
├── db.c
├── http.c
├── http_client.c
├── main.c
├── mix.c
├── sess.c
├── sip.c
├── social.c
├── source.c
├── stats.c
├── users.c
└── ws.c
├── tests
├── ccheck.py
└── phpunit
│ ├── .gitignore
│ ├── composer.json
│ ├── composer.lock
│ ├── phpunit.xml
│ └── tests
│ ├── ApiTest.php
│ ├── ChatTest.php
│ ├── Client.php
│ ├── LoginTest.php
│ └── TestCase.php
└── webui
├── .env.development
├── .env.production
├── .eslintrc.cjs
├── .gitignore
├── .prettierrc
├── .vscode
└── extensions.json
├── index.html
├── package-lock.json
├── package.json
├── public
├── avatars
│ ├── default.png
│ └── index.html
├── download
│ └── index.html
├── favicon.ico
├── robots.txt
└── sendegate.png
├── src
├── App.vue
├── api.ts
├── assets
│ └── vue.svg
├── avdummy.ts
├── components
│ ├── BottomActions.vue
│ ├── Calls.vue
│ ├── Chat.vue
│ ├── ErrorText.vue
│ ├── FooterLinks.vue
│ ├── Listeners.vue
│ ├── ReactionEmoji.vue
│ ├── RecButton.vue
│ ├── SettingsModal.vue
│ ├── Speakers.vue
│ ├── StudioNav.vue
│ ├── WebcamPhoto.vue
│ └── WebrtcVideo.vue
├── config.ts
├── error.ts
├── fadeout.ts
├── index.css
├── main.ts
├── router
│ └── index.ts
├── views
│ ├── FatalErrorView.vue
│ ├── HomeView.vue
│ ├── LoginView.vue
│ └── SocialLoginView.vue
├── vite-env.d.ts
├── webcam.ts
├── webrtc.ts
├── webrtc_source.ts
└── ws
│ └── state.ts
├── tailwind.config.cjs
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
/.clang-format:
--------------------------------------------------------------------------------
1 | ---
2 | Language: Cpp
3 | # BasedOnStyle: LLVM
4 | AccessModifierOffset: -2
5 | AlignAfterOpenBracket: Align
6 | AlignConsecutiveMacros: false
7 | AlignConsecutiveAssignments: true
8 | AlignConsecutiveDeclarations: false
9 | AlignEscapedNewlines: Right
10 | AlignOperands: true
11 | AlignTrailingComments: true
12 | AllowAllArgumentsOnNextLine: true
13 | AllowAllConstructorInitializersOnNextLine: true
14 | AllowAllParametersOfDeclarationOnNextLine: true
15 | AllowShortBlocksOnASingleLine: Never
16 | AllowShortCaseLabelsOnASingleLine: false
17 | AllowShortFunctionsOnASingleLine: None
18 | AllowShortLambdasOnASingleLine: All
19 | AllowShortIfStatementsOnASingleLine: Never
20 | AllowShortLoopsOnASingleLine: false
21 | AlwaysBreakAfterDefinitionReturnType: None
22 | AlwaysBreakAfterReturnType: None
23 | AlwaysBreakBeforeMultilineStrings: false
24 | AlwaysBreakTemplateDeclarations: MultiLine
25 | BinPackArguments: true
26 | BinPackParameters: true
27 | BraceWrapping:
28 | AfterCaseLabel: false
29 | AfterClass: true
30 | AfterControlStatement: false
31 | AfterEnum: false
32 | AfterFunction: true
33 | AfterNamespace: true
34 | AfterObjCDeclaration: false
35 | AfterStruct: false
36 | AfterUnion: true
37 | AfterExternBlock: false
38 | BeforeCatch: false
39 | BeforeElse: true
40 | IndentBraces: false
41 | SplitEmptyFunction: true
42 | SplitEmptyRecord: true
43 | SplitEmptyNamespace: true
44 | BreakBeforeBinaryOperators: None
45 | BreakBeforeBraces: Custom
46 | BreakBeforeInheritanceComma: false
47 | BreakInheritanceList: BeforeColon
48 | BreakBeforeTernaryOperators: true
49 | BreakConstructorInitializersBeforeComma: false
50 | BreakConstructorInitializers: BeforeColon
51 | BreakAfterJavaFieldAnnotations: false
52 | BreakStringLiterals: true
53 | ColumnLimit: 79
54 | CommentPragmas: '^ IWYU pragma:'
55 | CompactNamespaces: false
56 | ConstructorInitializerAllOnOneLineOrOnePerLine: false
57 | ConstructorInitializerIndentWidth: 8
58 | ContinuationIndentWidth: 8
59 | Cpp11BracedListStyle: true
60 | DeriveLineEnding: true
61 | DerivePointerAlignment: false
62 | DisableFormat: false
63 | ExperimentalAutoDetectBinPacking: false
64 | FixNamespaceComments: true
65 | ForEachMacros:
66 | - foreach
67 | - Q_FOREACH
68 | - BOOST_FOREACH
69 | IncludeBlocks: Preserve
70 | IncludeCategories:
71 | - Regex: '^"(llvm|llvm-c|clang|clang-c)/'
72 | Priority: 2
73 | SortPriority: 0
74 | - Regex: '^(<|"(gtest|gmock|isl|json)/)'
75 | Priority: 3
76 | SortPriority: 0
77 | - Regex: '.*'
78 | Priority: 1
79 | SortPriority: 0
80 | IncludeIsMainRegex: '(Test)?$'
81 | IncludeIsMainSourceRegex: ''
82 | IndentCaseLabels: false
83 | IndentGotoLabels: true
84 | IndentPPDirectives: None
85 | IndentWidth: 8
86 | IndentWrappedFunctionNames: false
87 | JavaScriptQuotes: Leave
88 | JavaScriptWrapImports: true
89 | KeepEmptyLinesAtTheStartOfBlocks: true
90 | MacroBlockBegin: ''
91 | MacroBlockEnd: ''
92 | MaxEmptyLinesToKeep: 2
93 | NamespaceIndentation: None
94 | ObjCBinPackProtocolList: Auto
95 | ObjCBlockIndentWidth: 2
96 | ObjCSpaceAfterProperty: false
97 | ObjCSpaceBeforeProtocolList: true
98 | PenaltyBreakAssignment: 2
99 | PenaltyBreakBeforeFirstCallParameter: 19
100 | PenaltyBreakComment: 300
101 | PenaltyBreakFirstLessLess: 120
102 | PenaltyBreakString: 1000
103 | PenaltyBreakTemplateDeclaration: 10
104 | PenaltyExcessCharacter: 1000000
105 | PenaltyReturnTypeOnItsOwnLine: 60
106 | PointerAlignment: Right
107 | ReflowComments: true
108 | SortIncludes: false
109 | SortUsingDeclarations: true
110 | SpaceAfterCStyleCast: false
111 | SpaceAfterLogicalNot: false
112 | SpaceAfterTemplateKeyword: true
113 | SpaceBeforeAssignmentOperators: true
114 | SpaceBeforeCpp11BracedList: false
115 | SpaceBeforeCtorInitializerColon: true
116 | SpaceBeforeInheritanceColon: true
117 | SpaceBeforeParens: ControlStatements
118 | SpaceBeforeRangeBasedForLoopColon: true
119 | SpaceInEmptyBlock: false
120 | SpaceInEmptyParentheses: false
121 | SpacesBeforeTrailingComments: 1
122 | SpacesInAngles: false
123 | SpacesInConditionalStatement: false
124 | SpacesInContainerLiterals: true
125 | SpacesInCStyleCastParentheses: false
126 | SpacesInParentheses: false
127 | SpacesInSquareBrackets: false
128 | SpaceBeforeSquareBrackets: false
129 | Standard: Latest
130 | StatementMacros:
131 | - Q_UNUSED
132 | - QT_REQUIRE_VERSION
133 | TabWidth: 8
134 | UseCRLF: false
135 | UseTab: Always
136 | ...
137 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | custom: ["https://studio-link.de/preise.html"]
4 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | webui:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - name: Use Node.js
15 | uses: actions/setup-node@v4
16 | with:
17 | node-version: '22'
18 | cache: 'npm'
19 | cache-dependency-path: 'webui/package-lock.json'
20 | - name: build
21 | run: |
22 | cd webui && npm install && npm run build && cd ..
23 | zip -r webui.zip webui/dist
24 | - uses: actions/upload-artifact@v4
25 | with:
26 | name: webui
27 | path: webui.zip
28 | retention-days: 1
29 |
30 | ccheck:
31 | runs-on: ubuntu-latest
32 | steps:
33 | - uses: actions/checkout@v4
34 | - name: ccheck
35 | run: |
36 | make ccheck
37 |
38 | test:
39 | runs-on: ${{ matrix.os }}
40 | needs: webui
41 | strategy:
42 | matrix:
43 | os: [ubuntu-24.04]
44 | steps:
45 | - uses: actions/checkout@v4
46 |
47 | - name: install clang-18 and ninja
48 | if: ${{ runner.os == 'Linux' }}
49 | run: |
50 | wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -
51 | sudo add-apt-repository "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-18 main"
52 | sudo apt-get update
53 | sudo apt-get install -y clang-tools-18 clang-18 clang-tidy-18 ninja-build cppcheck gdb
54 |
55 | - name: install libs
56 | if: ${{ runner.os == 'Linux' }}
57 | run: |
58 | sudo apt-get install -y libgd-dev libopus-dev libz-dev libssl-dev \
59 | libavformat-dev libavcodec-dev libswscale-dev libflac-dev liblmdb-dev \
60 | libswresample-dev libavfilter-dev libavdevice-dev
61 |
62 | - uses: actions/download-artifact@v4
63 | with:
64 | name: webui
65 |
66 | - name: make StudioLink - Linux
67 | if: ${{ runner.os == 'Linux' }}
68 | run: CC=clang-18 make
69 |
70 | - name: test requirements
71 | run: cd tests/phpunit && composer install
72 |
73 | - name: make test
74 | run: make test
75 |
76 | - name: clang-tidy
77 | if: ${{ runner.os == 'Linux' }}
78 | run: |
79 | clang-tidy-18 -p build -checks=cert-\*,-cert-dcl37-c,-cert-dcl51-cpp,-clang-analyzer-valist.Uninitialized src/*.c
80 | clang-tidy-18 -p build -checks=cert-\*,-cert-dcl37-c,-cert-dcl51-cpp,-clang-analyzer-valist.Uninitialized modules/**/*.c
81 |
82 | - name: clang scan-build
83 | if: ${{ runner.os == 'Linux' }}
84 | run: make clean && scan-build-18 --status-bugs make
85 |
86 | analyze:
87 | name: CodeQL Analyze
88 | runs-on: ubuntu-24.04
89 | needs: webui
90 |
91 | steps:
92 | - name: Checkout repository
93 | uses: actions/checkout@v4
94 |
95 | - name: install and ninja
96 | run: |
97 | sudo apt-get update && sudo apt-get install -y ninja-build
98 |
99 | - name: install libs
100 | if: ${{ runner.os == 'Linux' }}
101 | run: |
102 | sudo apt-get install -y libgd-dev libopus-dev libz-dev libssl-dev \
103 | libavformat-dev libavcodec-dev libswscale-dev libflac-dev liblmdb-dev \
104 | libswresample-dev libavfilter-dev libavdevice-dev
105 |
106 | - uses: actions/cache@v4
107 | with:
108 | path: third_party
109 | key: ${{ runner.os }}-gcc-${{ hashFiles('versions.mk') }}
110 |
111 | - uses: actions/download-artifact@v4
112 | with:
113 | name: webui
114 |
115 | - name: unzip webui
116 | run: rm -Rf webui && unzip webui.zip
117 |
118 | - run: make && make clean
119 |
120 | - name: Initialize CodeQL
121 | uses: github/codeql-action/init@v3
122 | with:
123 | languages: cpp
124 | queries: security-extended
125 |
126 | - run: |
127 | make
128 |
129 | - name: Perform CodeQL Analysis
130 | uses: github/codeql-action/analyze@v3
131 |
--------------------------------------------------------------------------------
/.github/workflows/docker.yml:
--------------------------------------------------------------------------------
1 | name: docker
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | paths:
7 | - 'Dockerfile'
8 | - 'entrypoint.sh'
9 | - '.github/workflows/docker.yml'
10 |
11 | env:
12 | VERSION_SLMIX: v1.0.0-beta
13 |
14 | jobs:
15 | docker:
16 | env:
17 | IMAGE_NAME: studio-link/mix/slmix
18 | runs-on: ubuntu-latest
19 | steps:
20 | - uses: actions/checkout@v4
21 | - name: Log into registry
22 | uses: docker/login-action@v3
23 | with:
24 | registry: "ghcr.io"
25 | username: ${{ github.actor }}
26 | password: ${{ secrets.GITHUB_TOKEN }}
27 |
28 | - name: Build
29 | run: |
30 | docker build -t slmix .
31 |
32 | - name: Tag and Push image
33 | if: github.event_name != 'pull_request'
34 | run: |
35 | docker tag slmix ghcr.io/${{ env.IMAGE_NAME }}:${{ env.VERSION_SLMIX }}
36 | docker tag slmix ghcr.io/${{ env.IMAGE_NAME }}:latest
37 | docker push ghcr.io/${{ env.IMAGE_NAME }}:${{ env.VERSION_SLMIX }}
38 | docker push ghcr.io/${{ env.IMAGE_NAME }}:latest
39 |
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /build*
2 |
3 | /external
4 |
5 | # clangd
6 | .cache
7 |
8 | # Vim swp files
9 | *.swp
10 |
11 | /database
12 |
13 | /webui/dist
14 |
15 | /webui/public/avatars/*
16 | !/webui/public/avatars/index.html
17 | !/webui/public/avatars/default.png
18 |
19 | /webui/public/download/*
20 | !/webui/public/download/index.html
21 |
22 | .gdb_history
23 |
24 | node_modules
25 |
26 | re_trace.json
27 |
--------------------------------------------------------------------------------
/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | #
2 | # CMakeLists.txt
3 | #
4 | # Copyright (C) 2022 Sebastian Reimers
5 | #
6 |
7 | ##############################################################################
8 | #
9 | # Project and Versioning
10 | #
11 |
12 | cmake_minimum_required(VERSION 3.18)
13 |
14 | set(CMAKE_C_COMPILER clang)
15 |
16 | project(slmix VERSION 1.0.0 LANGUAGES C)
17 |
18 | ##############################################################################
19 | #
20 | # Packages
21 | #
22 |
23 | list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/cmake)
24 |
25 | find_package(LMDB REQUIRED)
26 |
27 | ##############################################################################
28 | #
29 | # Compile options
30 | #
31 |
32 | option(USE_SD_SOCK "Enable systemd socket" OFF)
33 | option(USE_UNIX_SOCK "Enable UNIX socket" OFF)
34 |
35 | set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
36 |
37 | if(MSVC)
38 | add_compile_options("/W3")
39 | else()
40 | add_compile_options(
41 | -pedantic
42 | -Wall
43 | -Wbad-function-cast
44 | -Wcast-align
45 | -Wextra
46 | -Wmissing-declarations
47 | -Wmissing-prototypes
48 | -Wnested-externs
49 | -Wno-strict-aliasing
50 | -Wold-style-definition
51 | -Wshadow -Waggregate-return
52 | -Wstrict-prototypes
53 | -Wuninitialized
54 | -Wvla
55 | -Wno-gnu-zero-variadic-macro-arguments
56 | -Wno-c2x-extensions
57 | )
58 | endif()
59 |
60 | if(CMAKE_C_COMPILER_ID MATCHES "Clang")
61 | add_compile_options(-Wshorten-64-to-32 -Watomic-implicit-seq-cst)
62 | endif()
63 |
64 | if(USE_SD_SOCK)
65 | list(APPEND MIX_DEFS SLMIX_SD_SOCK)
66 | list(APPEND LINKLIBS systemd)
67 | endif()
68 |
69 | if(USE_UNIX_SOCK)
70 | list(APPEND MIX_DEFS SLMIX_UNIX_SOCK)
71 | endif()
72 |
73 | list(APPEND LINKLIBS gd ${LMDB_LIBRARIES})
74 |
75 | ##############################################################################
76 | #
77 | # Subdirectory section
78 | #
79 |
80 | set(RE_LIBRARY re CACHE STRING "re_library")
81 | set(REM_LIBRARY rem CACHE STRING "rem_library")
82 | set(STATIC ON CACHE BOOL "Build static")
83 | set(APP_MODULES_DIR ${CMAKE_SOURCE_DIR}/modules)
84 | set(APP_MODULES amix vmix)
85 | set(MODULES ice dtls_srtp turn opus vp8 avcodec fakevideo auresamp CACHE STRING "")
86 |
87 | add_subdirectory(external/re EXCLUDE_FROM_ALL)
88 | add_subdirectory(external/baresip EXCLUDE_FROM_ALL)
89 |
90 | find_package(re CONFIG REQUIRED)
91 |
92 | list(APPEND RE_DEFINITIONS
93 | _GNU_SOURCE
94 | )
95 |
96 | include_directories(
97 | include
98 | external/re/include
99 | external/baresip/include
100 | )
101 |
102 | ##############################################################################
103 | #
104 | # Source/Header section
105 | #
106 |
107 | set(SRCS_LIB
108 | src/avatar.c
109 | src/chat.c
110 | src/db.c
111 | src/http.c
112 | src/http_client.c
113 | src/mix.c
114 | src/sess.c
115 | src/sip.c
116 | src/social.c
117 | src/source.c
118 | src/stats.c
119 | src/users.c
120 | src/ws.c
121 | ${CMAKE_CURRENT_BINARY_DIR}/version.c
122 | )
123 |
124 | set(SRCS_EXE
125 | src/main.c
126 | )
127 |
128 |
129 | ##############################################################################
130 | #
131 | # Target objects
132 | #
133 |
134 | add_library(${PROJECT_NAME}-lib STATIC ${SRCS_LIB})
135 | target_link_libraries(${PROJECT_NAME}-lib PRIVATE ${LINKLIBS} re)
136 | target_compile_definitions(${PROJECT_NAME}-lib PRIVATE ${RE_DEFINITIONS} ${MIX_DEFS}
137 | SLMIX_VERSION="${PROJECT_VERSION}")
138 |
139 | add_executable(${PROJECT_NAME} ${SRCS_EXE})
140 | target_link_libraries(${PROJECT_NAME} PRIVATE slmix-lib baresip)
141 | target_compile_definitions(${PROJECT_NAME} PRIVATE ${RE_DEFINITIONS} ${MIX_DEFS}
142 | SLMIX_VERSION="${PROJECT_VERSION}")
143 |
144 | ADD_CUSTOM_COMMAND(
145 | OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/version.c
146 | ${CMAKE_CURRENT_BINARY_DIR}/_version.c
147 | COMMAND ${CMAKE_COMMAND} -P
148 | ${CMAKE_CURRENT_SOURCE_DIR}/cmake/version.cmake)
149 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | .
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM archlinux:latest as build
2 |
3 | RUN pacman -Syu --noconfirm curl wget ninja pkgconf clang cmake make git \
4 | patch ca-certificates gd opus zlib ffmpeg flac nodejs npm lmdb
5 | COPY . /opt/mix_build
6 | RUN cd /opt/mix_build && make release && \
7 | cd /opt/mix_build && make webui && \
8 | mkdir -p /opt/slmix/webui/public && \
9 | cp -a /opt/mix_build/build /opt/slmix/ && \
10 | cp -a /opt/mix_build/webui/dist /opt/slmix/webui/
11 |
12 | # --- Final image ---
13 | FROM archlinux:latest
14 |
15 | COPY --from=build /opt/slmix /opt/slmix
16 |
17 | RUN pacman -Syu --noconfirm ca-certificates gd opus zlib ffmpeg flac lmdb \
18 | nginx supervisor sudo && \
19 | yes | pacman -Scc && \
20 | useradd slmix -d /opt/slmix -s /bin/bash -u 16371 -U && \
21 | mkdir -p /opt/slmix/webui/public/avatars && \
22 | mkdir -p /opt/slmix/webui/public/downloads && \
23 | mkdir -p /opt/slmix/webui/database && \
24 | chown -R slmix:slmix /opt/slmix && \
25 | echo 'slmix ALL=(ALL) NOPASSWD:SETENV: /usr/bin/supervisord' >> /etc/sudoers
26 |
27 | COPY docker/supervisord.conf /etc/supervisord.conf
28 | COPY docker/nginx.conf /etc/nginx/nginx.conf
29 |
30 | VOLUME /opt/slmix/webui/public/avatars
31 | VOLUME /opt/slmix/webui/public/downloads
32 | VOLUME /opt/slmix/webui/database
33 |
34 | USER slmix
35 |
36 | ADD entrypoint.sh /entrypoint.sh
37 |
38 | EXPOSE 80/tcp
39 |
40 | ENTRYPOINT ["/entrypoint.sh"]
41 | CMD ["slmix"]
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2013-2025 Studio Link - Sebastian Reimers
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | ##############################################################################
2 | #
3 | # Build
4 | #
5 |
6 | .PHONY: build
7 | build: external
8 | @[ -f "build/build.ninja" ] || cmake -B build -G Ninja -DUSE_TRACE=ON -DUSE_TLS1_3_PHA=OFF \
9 | -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_FLAGS="-gdwarf-4 -g3 -DJBUF_STAT -DRE_RTP_PCAP"
10 | @cmake --build build --parallel
11 |
12 | .PHONY: webui
13 | webui:
14 | cd webui && npm install && npm run build-only
15 | rm -Rf webui/dist/avatars
16 | rm -Rf webui/dist/download
17 | mkdir -p webui/public/avatars
18 | ln -s ../public/avatars webui/dist/
19 | ln -s ../public/download webui/dist/
20 |
21 | .PHONY: release
22 | release: external
23 | make clean
24 | cmake -B build -GNinja -DCMAKE_BUILD_TYPE=Release \
25 | -DCMAKE_C_FLAGS="-g -DJBUF_STAT" -DUSE_TLS1_3_PHA=OFF
26 | make build
27 |
28 | .PHONY: systemd
29 | systemd: external
30 | make clean
31 | cmake -B build -GNinja -DCMAKE_BUILD_TYPE=Release \
32 | -DCMAKE_C_FLAGS="-g -DJBUF_STAT" -DUSE_SD_SOCK=ON -DUSE_TLS1_3_PHA=OFF
33 | make build
34 |
35 | .PHONY: unix
36 | unix: external
37 | make clean
38 | cmake -B build -GNinja -DCMAKE_BUILD_TYPE=Release \
39 | -DCMAKE_C_FLAGS="-g -DJBUF_STAT" -DUSE_UNIX_SOCK=ON -DUSE_TLS1_3_PHA=OFF
40 | make build
41 |
42 | external:
43 | mkdir -p external
44 | git clone --depth 1 \
45 | https://github.com/baresip/re.git external/re
46 | git clone --depth 1 -b playout_time \
47 | https://github.com/baresip/baresip.git external/baresip
48 | cd external/re && \
49 | patch -p1 < ../../patches/re_aubuf_timestamp_order_fix.patch
50 |
51 | ##############################################################################
52 | #
53 | # Sanitizers
54 | #
55 |
56 | .PHONY: run_san
57 | run_san:
58 | ASAN_OPTIONS=fast_unwind_on_malloc=0 \
59 | # TSAN_OPTIONS="suppressions=tsan.supp" \
60 | make run
61 |
62 | .PHONY: asan
63 | asan: external
64 | make clean
65 | cmake -B build -GNinja -DCMAKE_BUILD_TYPE=Debug \
66 | -DCMAKE_C_FLAGS="-fsanitize=undefined,address \
67 | -fno-omit-frame-pointer" \
68 | -DHAVE_THREADS=
69 | make build
70 |
71 | .PHONY: tsan
72 | tsan:
73 | make clean
74 | cmake -B build -GNinja -DCMAKE_BUILD_TYPE=Debug \
75 | -DCMAKE_C_FLAGS="-fsanitize=undefined,thread \
76 | -fno-omit-frame-pointer" \
77 | -DHAVE_THREADS=
78 | make build
79 |
80 |
81 | ##############################################################################
82 | #
83 | # Helpers
84 | #
85 |
86 | .PHONY: cloc
87 | cloc:
88 | cloc --exclude-dir='node_modules,external,build,env' --exclude-ext='json' .
89 |
90 | .PHONY: update
91 | update: external
92 | cd external/re && git pull
93 | cd external/rem && git pull
94 | cd external/baresip && git pull
95 |
96 | .PHONY: avatars
97 | avatars:
98 | mkdir -p webui/public/avatars
99 |
100 | .PHONY: run
101 | run: build avatars
102 | build/slmix -c config_example
103 |
104 | .PHONY: gdb
105 | gdb: build avatars
106 | gdb --args build/slmix -c config_example
107 |
108 | .PHONY: clean
109 | clean:
110 | rm -Rf build
111 |
112 | .PHONY: cleaner
113 | cleaner: clean
114 | rm -Rf external
115 |
116 | .PHONY: fresh
117 | fresh: clean build
118 |
119 | .PHONY: ccheck
120 | ccheck:
121 | tests/ccheck.py src modules
122 |
123 | .PHONY: test
124 | test:
125 | cd tests/phpunit && vendor/bin/phpunit
126 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Studio Link - Mix Rooms
2 |
3 | Remote Podcasting and live video/audio conversation with your audience
4 |
5 | ## Hosted version
6 |
7 | See https://mix.studio.link/hosted/started
8 |
9 | ## Documentation/Installation
10 |
11 | See https://mix.studio.link/self-hosting/install-intro
12 |
13 | ## Development
14 |
15 | Compile and start backend
16 |
17 | ```sh
18 | $ make
19 | $ build/slmix
20 | ```
21 |
22 | Start frontend
23 |
24 | ```sh
25 | $ cd webui
26 | $ npm install
27 | $ npm run dev
28 | ```
29 |
30 | ## License
31 |
32 | [MIT](https://github.com/Studio-Link/mix/blob/main/LICENSE)
33 |
--------------------------------------------------------------------------------
/cmake/FindLMDB.cmake:
--------------------------------------------------------------------------------
1 | find_path(LMDB_INCLUDE_DIR NAMES lmdb.h HINTS include)
2 | find_library(LMDB_LIBRARIES NAMES lmdb HINTS lib)
3 |
4 | include(FindPackageHandleStandardArgs)
5 | find_package_handle_standard_args(LMDB DEFAULT_MSG LMDB_INCLUDE_DIR LMDB_LIBRARIES)
6 |
7 | mark_as_advanced(LMDB_INCLUDE_DIR LMDB_LIBRARIES)
8 |
--------------------------------------------------------------------------------
/cmake/version.cmake:
--------------------------------------------------------------------------------
1 | execute_process(COMMAND git log --pretty=format:'%h' -n 1
2 | OUTPUT_VARIABLE GIT_REV
3 | ERROR_QUIET)
4 |
5 | # Check whether we got any revision (which isn't
6 | # always the case, e.g. when someone downloaded a zip
7 | # file from Github instead of a checkout)
8 | if ("${GIT_REV}" STREQUAL "")
9 | set(GIT_REV "N/A")
10 | set(GIT_DIFF "")
11 | set(GIT_TAG "N/A")
12 | set(GIT_BRANCH "N/A")
13 | else()
14 | execute_process(
15 | COMMAND bash -c "git diff --quiet --exit-code || echo +"
16 | OUTPUT_VARIABLE GIT_DIFF)
17 | execute_process(
18 | COMMAND git describe --exact-match --tags
19 | OUTPUT_VARIABLE GIT_TAG ERROR_QUIET)
20 | execute_process(
21 | COMMAND git rev-parse --abbrev-ref HEAD
22 | OUTPUT_VARIABLE GIT_BRANCH)
23 |
24 | string(STRIP "${GIT_REV}" GIT_REV)
25 | string(SUBSTRING "${GIT_REV}" 1 7 GIT_REV)
26 | string(STRIP "${GIT_DIFF}" GIT_DIFF)
27 | string(STRIP "${GIT_TAG}" GIT_TAG)
28 | string(STRIP "${GIT_BRANCH}" GIT_BRANCH)
29 | endif()
30 |
31 | #set(VERSION "const char* GIT_REV=\"${GIT_REV}${GIT_DIFF}\";
32 | set(VERSION "const char* GIT_REV=\"${GIT_REV}\";
33 | const char* GIT_TAG=\"${GIT_TAG}\";
34 | const char* GIT_BRANCH=\"${GIT_BRANCH}\";\n")
35 |
36 | if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/version.c)
37 | file(READ ${CMAKE_CURRENT_SOURCE_DIR}/version.c VERSION_)
38 | else()
39 | set(VERSION_ "")
40 | endif()
41 |
42 | if (NOT "${VERSION}" STREQUAL "${VERSION_}")
43 | file(WRITE ${CMAKE_CURRENT_SOURCE_DIR}/version.c "${VERSION}")
44 | endif()
45 |
--------------------------------------------------------------------------------
/config_example:
--------------------------------------------------------------------------------
1 | mix_room MixRoom
2 | mix_url /
3 | mix_token_host TOKENHOST # can start record
4 | mix_token_download TOKENDOWNLOAD # protected download folder
5 | mix_token_guests TOKENGUEST # invite url
6 | mix_token_api TOKENAPI # api token
7 | #mix_path /opt/slmix/
8 |
--------------------------------------------------------------------------------
/docker/nginx.conf:
--------------------------------------------------------------------------------
1 | worker_processes auto;
2 | daemon off;
3 | pid /var/run/nginx.pid;
4 |
5 |
6 | events {
7 | worker_connections 1024;
8 | }
9 |
10 |
11 | http {
12 | include mime.types;
13 | default_type application/octet-stream;
14 |
15 | server_tokens off;
16 | sendfile on;
17 | tcp_nopush on;
18 | tcp_nodelay on;
19 |
20 | server {
21 | listen 80 default_server;
22 | server_name _;
23 |
24 | add_header X-XSS-Protection "1; mode=block";
25 | add_header X-Content-Type-Options "nosniff";
26 |
27 | root /opt/slmix/webui/dist;
28 |
29 | location /api {
30 | proxy_pass http://127.0.0.1:9999;
31 | proxy_set_header X-Forwarded-Host $host;
32 | proxy_set_header X-Forwarded-Server $host;
33 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
34 | proxy_set_header Host $http_host;
35 | }
36 |
37 | location /ws {
38 | proxy_pass http://127.0.0.1:9999;
39 | proxy_redirect off;
40 |
41 | # Allow the use of websockets
42 | proxy_http_version 1.1;
43 | proxy_set_header Upgrade $http_upgrade;
44 | proxy_set_header Connection 'upgrade';
45 | proxy_set_header Host $host;
46 | proxy_cache_bypass $http_upgrade;
47 | }
48 |
49 | location ~* \.(?:ico|css|js|gif|jpe?g|png|webp)$ {
50 | expires 90d;
51 | add_header Vary Accept-Encoding;
52 | access_log off;
53 | }
54 |
55 | location / {
56 | expires off;
57 | add_header Cache-Control "public, max-age=0, s-maxage=0, must-revalidate" always;
58 | try_files $uri /index.html =404;
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/docker/supervisord.conf:
--------------------------------------------------------------------------------
1 | [unix_http_server]
2 | file=/tmp/supervisor.sock
3 |
4 | [supervisord]
5 | logfile=/dev/stdout
6 | logfile_maxbytes=0
7 | loglevel=info
8 | pidfile=/tmp/supervisord.pid
9 | nodaemon=false
10 | minfds=1024
11 | minprocs=200
12 |
13 | [rpcinterface:supervisor]
14 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
15 |
16 | [supervisorctl]
17 | serverurl=unix:///tmp/supervisor.sock
18 |
19 | [program:nginx]
20 | command=/usr/bin/nginx
21 | autostart=true
22 | autorestart=true
23 | priority=10
24 | stdout_logfile=/dev/fd/1
25 | stdout_logfile_maxbytes=0
26 | stderr_logfile=/dev/fd/2
27 | stderr_logfile_maxbytes=0
28 |
29 | [program:slmix]
30 | command=/opt/slmix/build/slmix -c /opt/slmix/config
31 | autostart=true
32 | autorestart=true
33 | priority=5
34 | user=slmix
35 | group=slmix
36 | directory=/opt/slmix
37 | stdout_logfile=/dev/fd/1
38 | stdout_logfile_maxbytes=0
39 | stderr_logfile=/dev/fd/2
40 | stderr_logfile_maxbytes=0
41 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | cache
3 |
--------------------------------------------------------------------------------
/docs/.vitepress/config.mts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitepress'
2 | import { en } from './en'
3 | import { de } from './de'
4 |
5 | // https://vitepress.dev/reference/site-config
6 | export default defineConfig({
7 | title: "Studio Link - Mix Rooms",
8 | description: "Mix Rooms",
9 | cleanUrls: true,
10 | themeConfig: {
11 | socialLinks: [
12 | { icon: 'github', link: 'https://github.com/studio-link/mix' },
13 | { icon: 'mastodon', link: 'https://social.studio.link/@social' }
14 | ],
15 | search: {
16 | provider: 'local'
17 | }
18 | },
19 | locales: {
20 | root: {
21 | label: 'English',
22 | ...en
23 | },
24 | de: {
25 | label: 'Deutsch',
26 | ...de
27 | }
28 | }
29 | })
30 |
--------------------------------------------------------------------------------
/docs/.vitepress/de.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, type DefaultTheme } from 'vitepress'
2 |
3 | export const de = defineConfig({
4 | lang: 'de-DE',
5 | description: 'Mix Rooms',
6 |
7 | themeConfig: {
8 | nav: nav(),
9 |
10 | sidebar: {
11 | '/de/hosted/': { base: '/de/hosted/', items: sidebarHosted() },
12 | '/de/self-hosting/': { base: '/de/self-hosting/', items: sidebarSelfHosted() }
13 | },
14 |
15 | footer: {
16 | message: 'Veröffentlicht unter der MIT Lizenz. Impressum | Datenschutz ',
17 | copyright: 'Copyright © 2013-heute IT-Service Sebastian Reimers'
18 | }
19 | }
20 | })
21 |
22 | function nav(): DefaultTheme.NavItem[] {
23 | return [
24 | {
25 | text: 'Jetzt loslegen',
26 | link: '/de/hosted/started',
27 | activeMatch: '/de/hosted/'
28 | },
29 | {
30 | text: 'Open Source',
31 | link: '/de/self-hosting/install-intro',
32 | activeMatch: '/de/self-hosting/'
33 | },
34 | {
35 | text: '1.0.0-beta',
36 | items: [
37 | {
38 | text: 'Changelog',
39 | link: '/de/changelog'
40 | }
41 | ]
42 | }
43 | ]
44 | }
45 |
46 | function sidebarHosted(): DefaultTheme.SidebarItem[] {
47 | return [
48 | {
49 | text: 'Einführung',
50 | collapsed: false,
51 | items: [
52 | { text: 'Was ist Mix Rooms?', link: 'what-is-mix-rooms' },
53 | { text: 'Jetzt loslegen', link: 'started' },
54 | ]
55 | },
56 | {
57 | text: 'Bedienung - Howto',
58 | collapsed: false,
59 | base: '/de/hosted/howto/',
60 | items: [
61 | { text: 'Überblick', link: 'overview' },
62 | { text: 'Login', link: 'login' },
63 | { text: 'Aufnahmen', link: 'recording' },
64 | { text: 'Chat', link: 'chat' },
65 | ]
66 | },
67 | ]
68 | }
69 |
70 | function sidebarSelfHosted(): DefaultTheme.SidebarItem[] {
71 | return [
72 | {
73 | text: 'Self-Hosting',
74 | items: [
75 | {
76 | text: 'Installation',
77 | base: '/de/self-hosting/install-',
78 | collapsed: false,
79 | items: [
80 | { text: 'Einführung', link: 'intro' },
81 | { text: 'Docker', link: 'docker' },
82 | {
83 | text: 'Quellcode',
84 | base: '/de/self-hosting/install-source-',
85 | items: [
86 | { text: 'Ubuntu 24.04', link: 'ubuntu24_04' },
87 | { text: 'Arch Linux', link: 'archlinux' },
88 | { text: 'Bauen und konfigurieren', link: 'build' },
89 | { text: 'Update', link: 'update' },
90 | ]
91 | },
92 | ]
93 |
94 | }
95 | ]
96 | }
97 | ]
98 | }
99 |
--------------------------------------------------------------------------------
/docs/.vitepress/en.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, type DefaultTheme } from 'vitepress'
2 |
3 | export const en = defineConfig({
4 | lang: 'en-US',
5 | description: 'Mix Rooms',
6 |
7 | themeConfig: {
8 | nav: nav(),
9 |
10 | sidebar: {
11 | '/hosted/': { base: '/hosted/', items: sidebarHosted() },
12 | '/self-hosting/': { base: '/self-hosting/', items: sidebarSelfHosted() }
13 | },
14 |
15 | footer: {
16 | message: 'Released under the MIT License. About | Privacy Policy ',
17 | copyright: 'Copyright © 2013-present IT-Service Sebastian Reimers'
18 | }
19 | }
20 | })
21 |
22 | function nav(): DefaultTheme.NavItem[] {
23 | return [
24 | {
25 | text: 'Get started',
26 | link: '/hosted/started',
27 | activeMatch: '/hosted/'
28 | },
29 | {
30 | text: 'Open Source',
31 | link: '/self-hosting/install-intro',
32 | activeMatch: '/self-hosting/'
33 | },
34 | {
35 | text: '1.0.0-beta',
36 | items: [
37 | {
38 | text: 'Changelog',
39 | link: '/changelog'
40 | }
41 | ]
42 | }
43 | ]
44 | }
45 |
46 | function sidebarHosted(): DefaultTheme.SidebarItem[] {
47 | return [
48 | {
49 | text: 'Introduction',
50 | collapsed: false,
51 | items: [
52 | { text: 'What is Mix Rooms?', link: 'what-is-mix-rooms' },
53 | { text: 'Getting Started', link: 'started' },
54 | ]
55 | },
56 | {
57 | text: 'Usage - Howto',
58 | collapsed: false,
59 | base: '/hosted/howto/',
60 | items: [
61 | { text: 'Overview', link: 'overview' },
62 | { text: 'Login', link: 'login' },
63 | { text: 'Recording', link: 'recording' },
64 | { text: 'Chat', link: 'chat' },
65 | ]
66 | },
67 | ]
68 | }
69 |
70 | function sidebarSelfHosted(): DefaultTheme.SidebarItem[] {
71 | return [
72 | {
73 | text: 'Self-Hosting',
74 | items: [
75 | {
76 | text: 'Installation',
77 | base: '/self-hosting/install-',
78 | collapsed: false,
79 | items: [
80 | { text: 'Introduction', link: 'intro' },
81 | { text: 'Docker', link: 'docker' },
82 | {
83 | text: 'From source',
84 | base: '/self-hosting/install-source-',
85 | items: [
86 | { text: 'Ubuntu 24.04', link: 'ubuntu24_04' },
87 | { text: 'Arch Linux', link: 'archlinux' },
88 | { text: 'Build and configure', link: 'build' },
89 | { text: 'Update', link: 'update' },
90 | ]
91 | },
92 | ]
93 |
94 | }
95 | ]
96 | }
97 | ]
98 | }
99 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/custom.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --vp-c-brand-1: #EA7400;
3 | --vp-c-brand-2: #c66302;
4 | --vp-c-brand-3: #EA7400;
5 | }
6 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/index.mts:
--------------------------------------------------------------------------------
1 | import './tailwind.css'
2 | import DefaultTheme from 'vitepress/theme'
3 | import './custom.css'
4 |
5 | export default DefaultTheme
6 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/tailwind.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 |
--------------------------------------------------------------------------------
/docs/changelog.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 |
9 | ## v1.0.0-beta - 2025-05-31
10 |
11 | ### ⁂ Fediverse Login
12 |
13 | It is now possible to log in with a Fediverse login (e.g. Mastodon).
14 |
15 |
16 |
17 |
18 |
19 |
20 | ### 🎥 Improved video layouts
21 |
22 | The new video layouts try to make better use of the available space:
23 |
24 | 
25 |
26 |
(N = number of participants)
27 |
28 | ### 👤 Solo button
29 |
30 | A new **solo button** allows you to highlight individual participants - ideal for moderation and live editing.
31 |
32 | 
33 |
34 | ### 🪞 Mirror image (Selfview)
35 |
36 | Your own camera view is now displayed with less delay and mirrored.
37 |
38 | ### 🎛️ Camera settings for login avatar snapshots
39 |
40 | It is now possible to select a specific camera for avatar creation (login).
41 |
42 | ### Emoticon reactions
43 |
44 | There is now a possibility to react with emoticons:
45 |
46 |
47 |
48 |
49 |
50 |
51 | ### 📚 Documentation
52 |
53 | At https://mix.studio.link/hosted/started you will now find instructions for the hosted version and also for self-hosting: https://mix.studio.link/self-hosting/install-intro
54 |
55 | ### 📶 Poor connections - optimised
56 |
57 | Stability with fluctuating network quality has been further improved, especially for video connections.
58 |
59 | ### 🛠️ Further technical innovations & internal refactorings
60 |
61 | - Upgrade to **TailwindCSS 4** in the web interface
62 | - New **Content-Security-Policy** for better security
63 | - Docker image (self-hosting)
64 | - Refactorings in **API**, **WebRTC**, **HTTP routing** and much more.
65 |
66 | ## v0.6.0-beta - 2024-04-10
67 |
68 | ### Added/Changed
69 |
70 | - New documentation (German and English)
71 | - Basic SIP support
72 | - Improved docker image
73 | - Native ffmpeg audio and video mp4 recording
74 | - Improved video encode/decode handling
75 |
76 | ## v0.5.3-beta - 2023-08-15
77 |
78 | ### Added
79 |
80 | - Add rtcp statistics
81 | - Basic Dockerfile and entrypoint.sh
82 |
83 | ### Changed
84 |
85 | - Allow re-auth by login token
86 |
87 | ## v0.5.2-beta - 2023-04-09
88 |
89 | ### Fixed
90 |
91 | - Speaker state initialization
92 |
93 | ## v0.5.1-beta - 2023-04-09
94 |
95 | ### Added
96 |
97 | - Fullscreen user status
98 | - HTML5 Login validation
99 |
100 | ### Fixes
101 |
102 | - Speaker state after logout
103 |
104 | ## v0.5.0-beta - 2023-04-07
105 |
106 | ### Added
107 |
108 | - Audio only recording option
109 | - Auto video scaling
110 | - Persistent sessions
111 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/docs/de/changelog.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | Alle nennenswerten Änderungen an diesem Projekt werden in dieser Datei dokumentiert.
4 |
5 | Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | und dieses Projekt hält sich an die [Semantische Versionierung](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## v1.0.0-beta - 2025-05-31
9 |
10 | ### ⁂ Fediverse Login
11 |
12 | Es ist nun möglich sich auch mit einem Fediverse Login (z.B. Mastodon) einzuloggen.
13 |
14 |
15 |
16 |
17 |
18 |
19 | ### 🎥 Verbesserte Video-Layouts
20 |
21 | Die neuen Video Layouts versuchen die vorhandene Fläche besser auszunutzen:
22 |
23 | 
24 |
25 | (N = Anzahl der Teilnehmer*innen)
26 |
27 | ### 👤 Solo-Button
28 |
29 | Ein neuer **Solo-Button** erlaubt es, gezielt einzelne Teilnehmer\*innen hervorzuheben – ideal für Moderation und Live-Schnitt.
30 |
31 | 
32 |
33 | ### 🪞 Spiegelbild (Selfview)
34 |
35 | Die eigene Kameraansicht wird nun mit weniger Verzögerung und gespiegelt angezeigt.
36 |
37 | ### 🎛️ Kameraeinstellungen für Login-Avatar-Snapshots
38 |
39 | Es ist jetzt möglich gezielt eine Kamera für die Avatar Erstellung (Login) auszuwählen.
40 |
41 | ### Emoticon Reaktionen
42 |
43 | Es gibt nun eine Möglichkeit mit Emoticons zu reagieren:
44 |
45 |
46 |
47 |
48 |
49 |
50 | ### 📚 Dokumentation
51 |
52 | Unter https://mix.studio.link/hosted/started findet sich nun eine Anleitung für die Hosted Version und auch zum selber Hosten: https://mix.studio.link/self-hosting/install-intro
53 |
54 | ### 📶 Schlechten Verbindungen - optimiert
55 |
56 | Die Stabilität bei schwankender Netzqualität wurde weiter verbessert, insbesondere für Videoverbindungen.
57 |
58 | ### 🛠️ Weitere Technische Neuerungen & Interne Refactorings
59 |
60 | - Upgrade auf **TailwindCSS 4** im Webinterface
61 | - Neue **Content-Security-Policy** für bessere Sicherheit
62 | - Docker Image (Self-Hosting)
63 | - Refactorings in **API**, **WebRTC**, **HTTP-Routing** u. v. m.
64 |
65 |
66 | ## v0.5.3-beta - 2023-08-15
67 |
68 | ### Hinzugefügt
69 |
70 | - RTCP Statistiken
71 | - Grundlegende Dockerfile und entrypoint.sh
72 |
73 | ### Geändert
74 |
75 | - Neuer Login durch bestehendes Login-Token zulassen
76 |
77 | ## v0.5.2-beta - 2023-04-09
78 |
79 | ### Behoben
80 |
81 | - Anzeigeverbesserung wer gerade spricht
82 |
83 | ## v0.5.1-beta - 2023-04-09
84 |
85 | ### Hinzugefügt
86 |
87 | - Benutzerstatus im Vollbild ist nun sichtbar
88 | - HTML5 Login-Validierung
89 |
90 | ### Behebt
91 |
92 | - Sprecher-Status nach Logout
93 |
94 | ## v0.5.0-beta - 2023-04-07
95 |
96 | ### Hinzugefügt
97 |
98 | - Nur Audio Aufnahme (ohne Video)
99 | - Automatische Video-Skalierung
100 | - Dauerhafte Sitzungen
101 |
--------------------------------------------------------------------------------
/docs/de/hosted/howto/chat.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Studio-Link/mix/11ec61647941eadaf826e3385c2acd300928c26d/docs/de/hosted/howto/chat.md
--------------------------------------------------------------------------------
/docs/de/hosted/howto/login.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Studio-Link/mix/11ec61647941eadaf826e3385c2acd300928c26d/docs/de/hosted/howto/login.md
--------------------------------------------------------------------------------
/docs/de/hosted/howto/overview.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Studio-Link/mix/11ec61647941eadaf826e3385c2acd300928c26d/docs/de/hosted/howto/overview.md
--------------------------------------------------------------------------------
/docs/de/hosted/howto/recording.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Studio-Link/mix/11ec61647941eadaf826e3385c2acd300928c26d/docs/de/hosted/howto/recording.md
--------------------------------------------------------------------------------
/docs/de/hosted/started.md:
--------------------------------------------------------------------------------
1 | # Jetzt loslegen
2 |
3 | Der einfachste Weg, **Studio Link - Mix Rooms** zu nutzen, ist die gehostete Version
4 | (monatliches Abonnement benötigt).
5 |
6 | Zum Starten sind nur drei einfache Schritte erforderlich!
7 |
8 | ## 1. Studio Link Konto
9 |
10 | Um die gehostete Version zu nutzen, kann unter diesem Link ein Konto registriert werden:
11 |
12 | https://my.studio.link/login
13 |
14 | ## 2. Mix-Room erstellen
15 |
16 | Unter https://my.studio.link/mixrooms befindet sich eine Übersicht der aktuellen Räume
17 | und auch die Möglichkeit über **Create Room** einen neuen Raum anzulegen:
18 |
19 | 
20 |
21 | Wähle hier eine benutzerdefinierte Mix Room URL. Für die private Nutzung, eignet
22 | sich eine eher zufällige URL wie "meingeheimerpodcast42.mix.studio.link".
23 |
24 |
25 | ## 3. Einladungslinks
26 |
27 | 
28 |
29 | Es stehen folgende Links zur Verfügung:
30 |
31 | - Der **Hosts** Link gibt dir vollen Zugriff auf den Mixraum. Du kannst kontrollieren
32 | wie man teilnehmen kann...
33 |
34 | - Den **Guests** Link kannst du mit deinen Podcast-Gästen teilen
35 |
36 | - Unter dem **Download** Link findest du alle Aufnahmen
37 |
38 |
39 |
--------------------------------------------------------------------------------
/docs/de/hosted/what-is-mix-rooms.md:
--------------------------------------------------------------------------------
1 | # Was ist Mix Rooms?
2 | **Studio Link - Mix Rooms** ist ein innovatives Remote-Podcasting-Tool, das die Art und Weise, wie Podcaster\*innen zusammenarbeiten,
3 | aufnehmen und Inhalte produzieren, verbessern soll. Diese Plattform ist auf die Bedürfnisse von Podcastern\*innen zugeschnitten,
4 | die von verschiedenen Standorten aus arbeiten und/oder Gäste einladen möchten.
5 |
6 | ## Anwendungsfälle
7 | - **Remote-Podcast-Aufnahmestudio**
8 |
9 | Mix Rooms bietet ein virtuelles Aufnahmestudio, in das sich Moderator\*innen und Gäste per Fernzugriff einklinken können. Das
10 | Tool sorgt für **Video- und Audioaufnahmen** in Studioqualität, bei denen die
11 | Stimme jedes Teilnehmers klar und präzise wiedergegeben wird.
12 |
13 | - **Kollaboration in Echtzeit**
14 |
15 | Auch das Publikum erlebt die Aufnahme in Echtzeit (keine sekundenlangen Verzögerungen im Stream mehr).
16 | Podcaster\*innen können auch über den integrierten Chat mit dem Publikum kommunizieren.
17 |
--------------------------------------------------------------------------------
/docs/de/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | # https://vitepress.dev/reference/default-theme-home-page
3 | layout: home
4 |
5 | hero:
6 | name: "Studio Link"
7 | text: "Mix Rooms"
8 | tagline: Remote Podcasting und Live Video/Audio Gespräche mit deiner Community
9 | actions:
10 | - theme: brand
11 | text: Jetzt loslegen
12 | link: /de/hosted/started
13 | - theme: alt
14 | text: Self-Hosting Guide
15 | link: /de/self-hosting/install-intro
16 |
17 | features:
18 | - title: Spontane Einbindung in Echtzeit
19 | details: Hol die Community auf deine virtuelle Bühne oder in den Podcast
20 | - title: Open Source
21 | details: Du kannst dein Studio auch selber hosten
22 | link: /de/self-hosting/install-intro
23 | ---
24 |
25 |
--------------------------------------------------------------------------------
/docs/de/self-hosting/install-docker.md:
--------------------------------------------------------------------------------
1 | # Docker
2 |
3 | TBD
4 |
--------------------------------------------------------------------------------
/docs/de/self-hosting/install-intro.md:
--------------------------------------------------------------------------------
1 | # Self-Hosting Guide
2 |
3 | Because **Studio Link - Mix Rooms** is open source, you can host it yourself.
4 |
5 | ## Limitations
6 |
7 | - No automatic multiroom support (you have to host multiple instances)
8 |
--------------------------------------------------------------------------------
/docs/de/self-hosting/install-source-archlinux.md:
--------------------------------------------------------------------------------
1 | # Installation unter Arch Linux
2 |
3 | ## Pakete
4 |
5 | ```bash
6 | pacman -S nginx curl wget ninja pkgconf clang cmake make git patch ca-certificates \
7 | gd opus zlib ffmpeg flac nodejs npm lmdb libvpx
8 | ```
9 |
--------------------------------------------------------------------------------
/docs/de/self-hosting/install-source-build.md:
--------------------------------------------------------------------------------
1 | # Bauen und konfigurieren
2 |
3 | ## Checkout und make
4 |
5 | ```bash
6 | sudo git clone https://github.com/Studio-Link/mix.git /opt/slmix
7 | sudo useradd slmix -d /opt/slmix -s /bin/bash -U
8 | sudo chown -R slmix:slmix /opt/slmix
9 | sudo su - slmix
10 | make release
11 | make webui
12 | ```
13 |
14 | ## Config - /opt/slmix/config
15 |
16 | ```
17 | mix_token_host TOKENREPLACEME # can start record
18 | mix_token_guests TOKENREPLACEME # invite url
19 | mix_token_download TOKENREPLACEME # protected download folder
20 | ```
21 |
22 | ## Nginx Config
23 |
24 | ```nginx
25 | server {
26 | listen 443 http2 ssl;
27 | listen [::]:443 http2 ssl;
28 | server_name mix.example.net;
29 |
30 | ssl_certificate /path/to/signed_cert_plus_intermediates;
31 | ssl_certificate_key /path/to/private_key;
32 | ssl_trusted_certificate /path/to/root_CA_cert_plus_intermediates;
33 |
34 | ssl_stapling on;
35 | ssl_stapling_verify on;
36 |
37 | add_header X-XSS-Protection "1; mode=block";
38 | add_header X-Content-Type-Options "nosniff";
39 | add_header Strict-Transport-Security max-age=15768000;
40 |
41 | root /opt/slmix/webui/dist;
42 |
43 | location /api {
44 | proxy_pass http://127.0.0.1:9999;
45 | proxy_set_header X-Forwarded-Host $host;
46 | proxy_set_header X-Forwarded-Server $host;
47 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
48 | proxy_set_header Host $http_host;
49 | }
50 |
51 | location /ws {
52 | proxy_pass http://127.0.0.1:9999;
53 | proxy_redirect off;
54 |
55 | # Allow the use of websockets
56 | proxy_http_version 1.1;
57 | proxy_set_header Upgrade $http_upgrade;
58 | proxy_set_header Connection 'upgrade';
59 | proxy_set_header Host $host;
60 | proxy_cache_bypass $http_upgrade;
61 | }
62 |
63 | location ~* \.(?:ico|css|js|gif|jpe?g|png|webp)$ {
64 | expires 90d;
65 | add_header Vary Accept-Encoding;
66 | access_log off;
67 | }
68 |
69 | location / {
70 | expires off;
71 | add_header Cache-Control "public, max-age=0, s-maxage=0, must-revalidate" always;
72 | try_files $uri /index.html =404;
73 | }
74 | }
75 | ```
76 |
77 | ## Systemd (/etc/systemd/system/slmix.service)
78 | ```systemd
79 | [Unit]
80 | Description=slmix
81 | After=syslog.target network.target
82 |
83 | [Service]
84 | Type=simple
85 | User=slmix
86 | Group=slmix
87 | WorkingDirectory=/opt/slmix/
88 | ExecStart=/opt/slmix/build/slmix -c /opt/slmix/config
89 | LimitNOFILE=2048
90 |
91 | [Install]
92 | WantedBy=multi-user.target
93 | ```
94 |
95 | ```bash
96 | systemctl enable slmix
97 | systemctl start slmix
98 | ```
99 |
100 | ::: tip Weitere Informationen zur Bedienung
101 | [Bedienung - Howto](/de/hosted/howto/login)
102 | :::
103 |
--------------------------------------------------------------------------------
/docs/de/self-hosting/install-source-ubuntu24_04.md:
--------------------------------------------------------------------------------
1 | # Installation unter Ubuntu 24.04 LTS
2 |
3 | ## Packages
4 |
5 | ```bash
6 | sudo apt install nginx curl wget ninja-build pkg-config clang cmake make git patch ca-certificates \
7 | libgd-dev libopus-dev libz-dev libssl-dev libflac-dev liblmdb-dev \
8 | libavformat-dev libavcodec-dev libavfilter-dev libavdevice-dev
9 | ```
10 |
11 | ## Node.js v22.x LTS
12 |
13 | https://github.com/nodesource/distributions/blob/master/README.md#debinstall
14 |
15 |
--------------------------------------------------------------------------------
/docs/de/self-hosting/install-source-update.md:
--------------------------------------------------------------------------------
1 | ---
2 | next:
3 | text: 'Login'
4 | link: '/hosted/howto/login'
5 | ---
6 |
7 | # Update
8 |
9 | ```bash
10 | sudo su - slmix
11 | git pull
12 | make cleaner
13 | make release
14 | make webui
15 | ```
16 |
--------------------------------------------------------------------------------
/docs/hosted/howto/chat.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Studio-Link/mix/11ec61647941eadaf826e3385c2acd300928c26d/docs/hosted/howto/chat.md
--------------------------------------------------------------------------------
/docs/hosted/howto/login.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Studio-Link/mix/11ec61647941eadaf826e3385c2acd300928c26d/docs/hosted/howto/login.md
--------------------------------------------------------------------------------
/docs/hosted/howto/overview.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Studio-Link/mix/11ec61647941eadaf826e3385c2acd300928c26d/docs/hosted/howto/overview.md
--------------------------------------------------------------------------------
/docs/hosted/howto/recording.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Studio-Link/mix/11ec61647941eadaf826e3385c2acd300928c26d/docs/hosted/howto/recording.md
--------------------------------------------------------------------------------
/docs/hosted/started.md:
--------------------------------------------------------------------------------
1 | # Let's get started
2 |
3 | The easiest way to use **Studio Link - Mix Rooms** is the hosted version
4 | (monthly subscription required).
5 |
6 | You need only three simple steps to begin!
7 |
8 | ## 1. Register a Studio Link Account
9 |
10 | If you wan't to use the hosted version you have to register a Account here:
11 |
12 | https://my.studio.link/login
13 |
14 | ## 2. Create a Mix Room
15 |
16 | Go to https://my.studio.link/mixrooms and click **New room**
17 |
18 | 
19 |
20 | Choose a custom Mix Room URL like. For private usage, use
21 | a more random URL like "myprivatesecretpodcast42".
22 |
23 |
24 | ## 3. Room Links
25 |
26 | 
27 |
28 | Now you can share or login with the provides Links:
29 |
30 | - The **Hosts** Link gives you full access to the mix room. You can control
31 | how can participate...
32 |
33 |
34 | - You can share the **Guests** Link with your podcast guests
35 |
36 | - Under the **Download** Link you find all recordings
37 |
--------------------------------------------------------------------------------
/docs/hosted/what-is-mix-rooms.md:
--------------------------------------------------------------------------------
1 | # What is Mix Rooms?
2 |
3 | **Studio Link - Mix Rooms** is an innovative remote podcasting tool designed to revolutionize the way podcasters collaborate, record, and produce content. This platform caters to the needs of podcasters working from different locations, offering a seamless and feature-rich experience for creating high-quality podcasts remotely.
4 |
5 | ## Use Cases
6 |
7 | - **Remote Podcast Recording Studio**
8 |
9 | Mix Rooms provides a virtual recording studio where hosts, co-hosts, and guests can connect remotely. The tool ensures studio-quality audio recording, capturing each participant's voice with clarity and precision.
10 |
11 | - **Real-time Collaboration**
12 |
13 | Foster real-time collaboration during recording sessions with Mix Rooms.
14 | Podcasters can communicate using integrated chat or voice features, enhancing teamwork and connection irrespective of geographical distances.
15 |
16 |
17 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | # https://vitepress.dev/reference/default-theme-home-page
3 | layout: home
4 |
5 | hero:
6 | name: "Studio Link"
7 | text: "Mix Rooms"
8 | tagline: Remote Cloud Podcasting and Live Video/Audio conversations with your audience
9 | actions:
10 | - theme: brand
11 | text: Get started
12 | link: /hosted/started
13 | - theme: alt
14 | text: Self-Hosting Guide
15 | link: /self-hosting/install-intro
16 |
17 | features:
18 | - title: Realtime Communication
19 | details: Bring the community onto your virtual stage or into the podcast
20 | - title: Open Source
21 | details: No secrets, the code is available for free forever
22 | link: /self-hosting/install-intro
23 | ---
24 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "tailwindcss": "^4.1.7",
4 | "vitepress": "^1.6.3"
5 | },
6 | "scripts": {
7 | "dev": "vitepress dev .",
8 | "build": "vitepress build .",
9 | "preview": "vitepress preview ."
10 | },
11 | "dependencies": {
12 | "@tailwindcss/vite": "^4.1.7"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/docs/public/create_room.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Studio-Link/mix/11ec61647941eadaf826e3385c2acd300928c26d/docs/public/create_room.png
--------------------------------------------------------------------------------
/docs/public/emoticons.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Studio-Link/mix/11ec61647941eadaf826e3385c2acd300928c26d/docs/public/emoticons.mp4
--------------------------------------------------------------------------------
/docs/public/login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Studio-Link/mix/11ec61647941eadaf826e3385c2acd300928c26d/docs/public/login.png
--------------------------------------------------------------------------------
/docs/public/room_links.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Studio-Link/mix/11ec61647941eadaf826e3385c2acd300928c26d/docs/public/room_links.png
--------------------------------------------------------------------------------
/docs/public/social_login.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Studio-Link/mix/11ec61647941eadaf826e3385c2acd300928c26d/docs/public/social_login.mp4
--------------------------------------------------------------------------------
/docs/public/solo_button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Studio-Link/mix/11ec61647941eadaf826e3385c2acd300928c26d/docs/public/solo_button.png
--------------------------------------------------------------------------------
/docs/public/vidconv.drawio.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Studio-Link/mix/11ec61647941eadaf826e3385c2acd300928c26d/docs/public/vidconv.drawio.png
--------------------------------------------------------------------------------
/docs/self-hosting/install-docker.md:
--------------------------------------------------------------------------------
1 | # Docker
2 |
3 | ```bash
4 | docker pull ghcr.io/studio-link/mix/slmix:v1.0.0-beta
5 | docker run --name slmix -it -p8080:80 \
6 | -e TOKENHOST=host \
7 | -e TOKENDOWNLOAD=downloadsecret \
8 | -e TOKENGUEST=guest \
9 | -e TOKENAPI=1234 \
10 | slmix:v1.0.0-beta
11 | ```
12 |
13 | ## Nginx reverse proxy example
14 |
15 | ```nginx
16 | server {
17 | listen 80 default_server;
18 | listen [::]:80 ipv6only=on default_server;
19 | server_name slmix.mydomain.com;
20 | location / {
21 | return 301 https://$host$request_uri;
22 | }
23 | }
24 | server {
25 | listen 443 default_server;
26 | listen [::]:443 ipv6only=on default_server;
27 | server_name slmix.mydomain.com;
28 |
29 | ssl on;
30 | ssl_certificate /path/to/cert.chain.pem;
31 | ssl_certificate_key /path/to/key.pem;
32 |
33 | location /ws {
34 | proxy_pass http://localhost:8080;
35 | proxy_redirect off;
36 |
37 | # Allow the use of websockets
38 | proxy_http_version 1.1;
39 | proxy_set_header Upgrade $http_upgrade;
40 | proxy_set_header Connection 'upgrade';
41 | proxy_set_header Host $host;
42 | proxy_cache_bypass $http_upgrade;
43 | }
44 |
45 | location / {
46 | proxy_pass http://localhost:8080;
47 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
48 | proxy_set_header X-Forwarded-Proto https;
49 | proxy_set_header Host $http_host;
50 | }
51 | }
52 | ```
53 |
--------------------------------------------------------------------------------
/docs/self-hosting/install-intro.md:
--------------------------------------------------------------------------------
1 | # Self-Hosting Guide
2 |
3 | Because **Studio Link - Mix Rooms** is open source, you can host it yourself.
4 |
5 | ## Limitations
6 |
7 | - No automatic multiroom support (you have to host multiple instances)
8 |
--------------------------------------------------------------------------------
/docs/self-hosting/install-source-archlinux.md:
--------------------------------------------------------------------------------
1 | # Installation on Arch Linux
2 |
3 | ## Packages
4 |
5 | ```bash
6 | pacman -S nginx curl wget ninja pkgconf clang cmake make git patch ca-certificates \
7 | gd opus zlib ffmpeg flac nodejs npm lmdb libvpx
8 | ```
9 |
--------------------------------------------------------------------------------
/docs/self-hosting/install-source-build.md:
--------------------------------------------------------------------------------
1 | # Build and configure
2 |
3 | ## Checkout and make
4 |
5 | ```bash
6 | sudo git clone https://github.com/Studio-Link/mix.git /opt/slmix
7 | sudo useradd slmix -d /opt/slmix -s /bin/bash -U
8 | sudo chown -R slmix:slmix /opt/slmix
9 | sudo su - slmix
10 | make release
11 | make webui
12 | ```
13 |
14 | ## Config - /opt/slmix/config
15 |
16 | ```
17 | mix_token_host TOKENREPLACEME # can start record
18 | mix_token_guests TOKENREPLACEME # invite url
19 | mix_token_download TOKENREPLACEME # protected download folder
20 | ```
21 |
22 | ## Nginx Config
23 |
24 | ```nginx
25 | server {
26 | listen 443 ssl;
27 | listen [::]:443 ssl;
28 | server_name mix.example.net;
29 |
30 | http2 on;
31 |
32 | ssl_certificate /path/to/signed_cert_plus_intermediates;
33 | ssl_certificate_key /path/to/private_key;
34 | ssl_trusted_certificate /path/to/root_CA_cert_plus_intermediates;
35 |
36 | ssl_stapling on;
37 | ssl_stapling_verify on;
38 |
39 | add_header X-XSS-Protection "1; mode=block";
40 | add_header X-Content-Type-Options "nosniff";
41 | add_header Strict-Transport-Security max-age=15768000;
42 |
43 | root /opt/slmix/webui/dist;
44 |
45 | location /api {
46 | proxy_pass http://127.0.0.1:9999;
47 | proxy_set_header X-Forwarded-Host $host;
48 | proxy_set_header X-Forwarded-Server $host;
49 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
50 | proxy_set_header Host $http_host;
51 | }
52 |
53 | location /ws {
54 | proxy_pass http://127.0.0.1:9999;
55 | proxy_redirect off;
56 |
57 | # Allow the use of websockets
58 | proxy_http_version 1.1;
59 | proxy_set_header Upgrade $http_upgrade;
60 | proxy_set_header Connection 'upgrade';
61 | proxy_set_header Host $host;
62 | proxy_cache_bypass $http_upgrade;
63 | }
64 |
65 | location ~* \.(?:ico|css|js|gif|jpe?g|png|webp)$ {
66 | expires 90d;
67 | add_header Vary Accept-Encoding;
68 | access_log off;
69 | }
70 |
71 | location / {
72 | expires off;
73 | add_header Cache-Control "public, max-age=0, s-maxage=0, must-revalidate" always;
74 | try_files $uri /index.html =404;
75 | }
76 | }
77 | ```
78 |
79 | ## Systemd (/etc/systemd/system/slmix.service)
80 | ```systemd
81 | [Unit]
82 | Description=slmix
83 | After=syslog.target network.target
84 |
85 | [Service]
86 | Type=simple
87 | User=slmix
88 | Group=slmix
89 | WorkingDirectory=/opt/slmix/
90 | ExecStart=/opt/slmix/build/slmix -c /opt/slmix/config
91 | LimitNOFILE=2048
92 |
93 | [Install]
94 | WantedBy=multi-user.target
95 | ```
96 |
97 | ```bash
98 | systemctl enable slmix
99 | systemctl start slmix
100 | ```
101 |
102 | ::: tip More informations about usage
103 | [Usage - Howto](/hosted/howto/login)
104 | :::
105 |
--------------------------------------------------------------------------------
/docs/self-hosting/install-source-ubuntu24_04.md:
--------------------------------------------------------------------------------
1 | # Installation on Ubuntu 24.04 LTS
2 |
3 | ## Packages
4 |
5 | ```bash
6 | sudo apt install nginx curl wget ninja-build pkg-config clang cmake make git patch ca-certificates \
7 | libgd-dev libopus-dev libz-dev libssl-dev libflac-dev liblmdb-dev \
8 | libavformat-dev libavcodec-dev libavfilter-dev libavdevice-dev
9 | ```
10 |
11 | ## Node.js v22.x LTS
12 |
13 | https://github.com/nodesource/distributions/blob/master/README.md#debinstall
14 |
15 |
--------------------------------------------------------------------------------
/docs/self-hosting/install-source-update.md:
--------------------------------------------------------------------------------
1 | ---
2 | next:
3 | text: 'Login'
4 | link: '/hosted/howto/login'
5 | ---
6 |
7 | # Update
8 |
9 | ```bash
10 | sudo su - slmix
11 | git pull
12 | make cleaner
13 | make release
14 | make webui
15 | ```
16 |
--------------------------------------------------------------------------------
/docs/vite.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import tailwindcss from '@tailwindcss/vite'
3 |
4 | export default defineConfig({
5 | plugins: [
6 | tailwindcss(),
7 | ],
8 | })
9 |
--------------------------------------------------------------------------------
/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -e
3 |
4 | if [ "$1" = 'slmix' ]; then
5 | if [[ -z $TOKENHOST ]]; then
6 | echo "No TOKENHOST env!"
7 | exit 1
8 | fi
9 | if [[ -z $TOKENDOWNLOAD ]]; then
10 | echo "No TOKENDOWNLOAD env!"
11 | exit 1
12 | fi
13 | if [[ -z $TOKENGUEST ]]; then
14 | echo "No TOKENGUEST env!"
15 | exit 1
16 | fi
17 | if [[ -z $TOKENAPI ]]; then
18 | echo "No TOKENAPI env!"
19 | exit 1
20 | fi
21 | cat > /opt/slmix/config <
2 | #include
3 | #include
4 |
5 | #include
6 | #include
7 | #include
8 | #include "amix.h"
9 |
10 | struct flac {
11 | FLAC__StreamEncoder *enc;
12 | FLAC__StreamMetadata *m[2];
13 | FLAC__int32 *pcm;
14 | };
15 |
16 |
17 | static void flac_destruct(void *arg)
18 | {
19 | struct flac *flac = arg;
20 |
21 | mem_deref(flac->pcm);
22 | FLAC__stream_encoder_finish(flac->enc);
23 | FLAC__stream_encoder_delete(flac->enc);
24 | FLAC__metadata_object_delete(flac->m[0]);
25 | FLAC__metadata_object_delete(flac->m[1]);
26 | }
27 |
28 |
29 | int flac_init(struct flac **flacp, struct auframe *af, char *file)
30 | {
31 |
32 | struct flac *flac;
33 | FLAC__bool ret;
34 | FLAC__StreamEncoderInitStatus init;
35 | FLAC__StreamMetadata_VorbisComment_Entry entry;
36 |
37 |
38 | if (!flacp || !af || !file)
39 | return EINVAL;
40 |
41 | flac = mem_zalloc(sizeof(struct flac), flac_destruct);
42 | if (!flac)
43 | return ENOMEM;
44 |
45 | flac->pcm = mem_zalloc(af->sampc * sizeof(FLAC__int32), NULL);
46 |
47 |
48 | flac->enc = FLAC__stream_encoder_new();
49 | if (!flac->enc)
50 | return ENOMEM;
51 |
52 | ret = FLAC__stream_encoder_set_verify(flac->enc, true);
53 | ret &= FLAC__stream_encoder_set_compression_level(flac->enc, 5);
54 | ret &= FLAC__stream_encoder_set_channels(flac->enc, af->ch);
55 | ret &= FLAC__stream_encoder_set_bits_per_sample(flac->enc, 16);
56 | ret &= FLAC__stream_encoder_set_sample_rate(flac->enc, af->srate);
57 | ret &= FLAC__stream_encoder_set_total_samples_estimate(flac->enc, 0);
58 |
59 | if (!ret) {
60 | warning("record: FLAC__stream_encoder_set\n");
61 | return EINVAL;
62 | }
63 |
64 | /* METADATA */
65 | flac->m[0] =
66 | FLAC__metadata_object_new(FLAC__METADATA_TYPE_VORBIS_COMMENT);
67 | flac->m[1] = FLAC__metadata_object_new(FLAC__METADATA_TYPE_PADDING);
68 |
69 | ret = FLAC__metadata_object_vorbiscomment_entry_from_name_value_pair(
70 | &entry, "ENCODED_BY", "STUDIO LINK MIX");
71 |
72 | ret &= FLAC__metadata_object_vorbiscomment_append_comment(
73 | flac->m[0], entry, /*copy=*/false);
74 |
75 | if (!ret) {
76 | warning("record: FLAC METADATA ERROR: out of memory or tag "
77 | "error\n");
78 | return ENOMEM;
79 | }
80 |
81 | flac->m[1]->length = 1234; /* padding length */
82 |
83 | ret = FLAC__stream_encoder_set_metadata(flac->enc, flac->m, 2);
84 |
85 | if (!ret) {
86 | warning("record: FLAC__stream_encoder_set_metadata\n");
87 | return ENOMEM;
88 | }
89 |
90 | init = FLAC__stream_encoder_init_file(flac->enc, file, NULL, NULL);
91 |
92 | if (init != FLAC__STREAM_ENCODER_INIT_STATUS_OK) {
93 | warning("record: FLAC ERROR: initializing encoder: %s\n",
94 | FLAC__StreamEncoderInitStatusString[init]);
95 | return ENOMEM;
96 | }
97 |
98 | *flacp = flac;
99 |
100 | return 0;
101 | }
102 |
103 |
104 | int flac_record(struct flac *flac, struct auframe *af, uint64_t offset)
105 | {
106 | FLAC__StreamEncoderState state;
107 | FLAC__bool ret;
108 |
109 | if (!flac || !af || !af->ch)
110 | return EINVAL;
111 |
112 | if (offset > 24 * 3600 * 1000) {
113 | warning("flac_record: ignoring high >24h offset (%llu)\n",
114 | offset);
115 | offset = 0;
116 | }
117 |
118 | if (offset < 2 * PTIME) /* FIXME */
119 | offset = 0;
120 |
121 | if (offset) {
122 | info("flac_record: offset %llu id %u\n", offset, af->id);
123 | memset(flac->pcm, 0, af->sampc * sizeof(FLAC__int32));
124 | uint64_t offsampc = af->srate * af->ch * offset / 1000;
125 |
126 | while (offsampc) {
127 | ret = FLAC__stream_encoder_process_interleaved(
128 | flac->enc, flac->pcm,
129 | (uint32_t)af->sampc / af->ch);
130 | if (!ret)
131 | goto err;
132 |
133 | if (offsampc >= af->sampc)
134 | offsampc -= af->sampc;
135 | else {
136 | ret = FLAC__stream_encoder_process_interleaved(
137 | flac->enc, flac->pcm,
138 | (uint32_t)offsampc / af->ch);
139 | if (!ret)
140 | goto err;
141 | offsampc = 0;
142 | }
143 | }
144 | }
145 |
146 | int16_t *sampv = (int16_t *)af->sampv;
147 |
148 | for (size_t i = 0; i < af->sampc; i++) {
149 | flac->pcm[i] = sampv[i];
150 | }
151 |
152 | ret = FLAC__stream_encoder_process_interleaved(
153 | flac->enc, flac->pcm, (uint32_t)af->sampc / af->ch);
154 | if (ret)
155 | return 0;
156 |
157 |
158 | err:
159 | state = FLAC__stream_encoder_get_state(flac->enc);
160 | warning("record: FLAC ENCODE ERROR: %s\n",
161 | FLAC__StreamEncoderStateString[state]);
162 |
163 | return EBADFD;
164 | }
165 |
--------------------------------------------------------------------------------
/modules/amix/record.c:
--------------------------------------------------------------------------------
1 | /**
2 | * @file aumix/record.c aumix recording
3 | *
4 | * Copyright (C) 2022 Sebastian Reimers
5 | */
6 |
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 | #include "amix.h"
14 |
15 |
16 | static struct {
17 | struct list tracks;
18 | RE_ATOMIC bool run;
19 | thrd_t thread;
20 | struct aubuf *ab;
21 | char *folder;
22 | uint64_t msecs;
23 | } record = {.tracks = LIST_INIT, .run = false};
24 |
25 | struct record_entry {
26 | struct le le;
27 | struct mbuf *mb;
28 | size_t size;
29 | };
30 |
31 |
32 | uint64_t amix_record_msecs(void)
33 | {
34 | if (!record.msecs)
35 | return 0;
36 | return tmr_jiffies() - record.msecs;
37 | }
38 |
39 |
40 | struct track {
41 | struct le le;
42 | uint16_t id;
43 | char file[512];
44 | uint64_t last;
45 | struct flac *flac;
46 | };
47 |
48 |
49 | static void track_destruct(void *arg)
50 | {
51 | struct track *track = arg;
52 |
53 | list_unlink(&track->le);
54 | mem_deref(track->flac);
55 | }
56 |
57 |
58 | static int record_track(struct auframe *af)
59 | {
60 | struct le *le;
61 | struct track *track = NULL;
62 | uint64_t offset;
63 | int err;
64 |
65 | LIST_FOREACH(&record.tracks, le)
66 | {
67 | struct track *t = le->data;
68 |
69 | if (t->id == af->id) {
70 | track = t;
71 | break;
72 | }
73 | }
74 |
75 | if (!track) {
76 | track = mem_zalloc(sizeof(struct track), track_destruct);
77 | if (!track)
78 | return ENOMEM;
79 |
80 | track->id = af->id;
81 | track->last = record.msecs;
82 |
83 | /* TODO: add user->name */
84 | re_snprintf(track->file, sizeof(track->file),
85 | "%s/audio_id%u.flac", record.folder, track->id);
86 |
87 | err = flac_init(&track->flac, af, track->file);
88 | if (err) {
89 | mem_deref(track);
90 | return err;
91 | }
92 |
93 | list_append(&record.tracks, &track->le, track);
94 | }
95 |
96 | offset = af->timestamp - track->last;
97 | track->last = af->timestamp;
98 |
99 | flac_record(track->flac, af, offset);
100 |
101 | return 0;
102 | }
103 |
104 |
105 | static int record_thread(void *arg)
106 | {
107 | struct auframe af;
108 | int err;
109 | (void)arg;
110 |
111 | int16_t *sampv;
112 | size_t sampc = SRATE * CH * PTIME / 1000;
113 |
114 | sampv = mem_zalloc(sampc * sizeof(int16_t), NULL);
115 | if (!sampv)
116 | return ENOMEM;
117 |
118 | auframe_init(&af, AUFMT_S16LE, sampv, sampc, SRATE, CH);
119 |
120 | if (!record.msecs)
121 | record.msecs = tmr_jiffies();
122 |
123 | while (re_atomic_rlx(&record.run)) {
124 | sys_msleep(4);
125 | while (aubuf_cur_size(record.ab) > sampc) {
126 | aubuf_read_auframe(record.ab, &af);
127 | err = record_track(&af);
128 | if (err)
129 | goto out;
130 | }
131 | }
132 |
133 | out:
134 | record.msecs = 0;
135 | mem_deref(sampv);
136 |
137 | return 0;
138 | }
139 |
140 |
141 | int amix_record_start(const char *folder)
142 | {
143 | int err;
144 |
145 | if (!folder)
146 | return EINVAL;
147 |
148 | if (re_atomic_rlx(&record.run))
149 | return EALREADY;
150 |
151 | record.msecs = 0;
152 | str_dup(&record.folder, folder);
153 |
154 | err = aubuf_alloc(&record.ab, 0, 0);
155 | if (err) {
156 | return err;
157 | }
158 |
159 | re_atomic_rlx_set(&record.run, true);
160 | info("aumix: record started\n");
161 |
162 | thread_create_name(&record.thread, "aumix record", record_thread,
163 | NULL);
164 |
165 | return 0;
166 | }
167 |
168 |
169 | void amix_record(struct auframe *af)
170 | {
171 | if (!re_atomic_rlx(&record.run) || !af->id)
172 | return;
173 |
174 | af->timestamp = tmr_jiffies();
175 | aubuf_write_auframe(record.ab, af);
176 | }
177 |
178 |
179 | int amix_record_close(void)
180 | {
181 | if (!re_atomic_rlx(&record.run))
182 | return EINVAL;
183 |
184 | re_atomic_rlx_set(&record.run, false);
185 | info("aumix: record close\n");
186 | thrd_join(record.thread, NULL);
187 |
188 | mem_deref(record.ab);
189 |
190 | record.folder = mem_deref(record.folder);
191 | list_flush(&record.tracks);
192 |
193 | return 0;
194 | }
195 |
--------------------------------------------------------------------------------
/modules/vmix/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | project(vmix)
2 |
3 | set(SRCS vmix.c src.c disp.c record.c codec.c pktsrc.c)
4 |
5 | include_directories(
6 | ../../include
7 | )
8 |
9 | find_package(FFMPEG COMPONENTS
10 | avcodec avfilter avformat swscale swresample avdevice avutil)
11 |
12 | if(STATIC)
13 | add_library(${PROJECT_NAME} OBJECT ${SRCS})
14 | else()
15 | add_library(${PROJECT_NAME} MODULE ${SRCS})
16 | endif()
17 |
18 | target_link_libraries(${PROJECT_NAME} PRIVATE slmix-lib ${FFMPEG_LIBRARIES})
19 |
--------------------------------------------------------------------------------
/modules/vmix/disp.c:
--------------------------------------------------------------------------------
1 | /**
2 | * @file vidmix/disp.c vidmix -- display
3 | *
4 | * Copyright (C) 2021 Sebastian Reimers
5 | */
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include "vmix.h"
11 |
12 |
13 | static void destructor(void *arg)
14 | {
15 | struct vidisp_st *st = arg;
16 |
17 | if (st->vidsrc)
18 | st->vidsrc->vidisp = NULL;
19 |
20 | list_unlink(&st->le);
21 | mem_deref(st->device);
22 | }
23 |
24 |
25 | int vmix_disp_alloc(struct vidisp_st **stp, const struct vidisp *vd,
26 | struct vidisp_prm *prm, const char *dev,
27 | vidisp_resize_h *resizeh, void *arg)
28 | {
29 | struct vidisp_st *st;
30 | int err = 0;
31 | (void)prm;
32 | (void)resizeh;
33 | (void)arg;
34 |
35 | if (!stp || !vd || !dev)
36 | return EINVAL;
37 |
38 | st = mem_zalloc(sizeof(*st), destructor);
39 | if (!st)
40 | return ENOMEM;
41 |
42 | err = str_dup(&st->device, dev);
43 | if (err)
44 | goto out;
45 |
46 | /* find the vidsrc with the same device-name */
47 | st->vidsrc = vmix_src_find(dev);
48 | if (!st->vidsrc || !st->vidsrc->vidmix_src) {
49 | err = ENOKEY;
50 | return 0; /* FIXME */
51 | goto out;
52 | }
53 |
54 | st->vidsrc->vidisp = st;
55 | hash_append(vmix_disp, hash_joaat_str(dev), &st->le, st);
56 |
57 | out:
58 | if (err)
59 | mem_deref(st);
60 | else
61 | *stp = st;
62 |
63 | return err;
64 | }
65 |
66 |
67 | static bool list_apply_handler(struct le *le, void *arg)
68 | {
69 | struct vidisp_st *st = le->data;
70 |
71 | return 0 == str_cmp(st->device, arg);
72 | }
73 |
74 |
75 | int vmix_disp_display(struct vidisp_st *st, const char *title,
76 | const struct vidframe *frame, uint64_t timestamp)
77 | {
78 | int err = 0;
79 | (void)title;
80 |
81 | if (!st || !frame)
82 | return EINVAL;
83 |
84 | if (st->vidsrc)
85 | vmix_src_input(st->vidsrc, frame, timestamp);
86 | else {
87 | debug("vidmix: display: dropping frame (%u x %u)\n",
88 | frame->size.w, frame->size.h);
89 | }
90 |
91 | return err;
92 | }
93 |
94 |
95 | struct vidisp_st *vmix_disp_find(const char *device)
96 | {
97 | return list_ledata(hash_lookup(vmix_disp, hash_joaat_str(device),
98 | list_apply_handler, (void *)device));
99 | }
100 |
--------------------------------------------------------------------------------
/modules/vmix/pktsrc.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include "re_thread.h"
3 | #include "vmix.h"
4 |
5 | static struct {
6 | struct list srcl;
7 | thrd_t thrd;
8 | RE_ATOMIC bool run;
9 | struct vidsrc *vidsrc;
10 | mtx_t *mtx;
11 | } pktsrc;
12 |
13 |
14 | static void pktsrc_deref(void *arg)
15 | {
16 | struct vidsrc_st *st = arg;
17 | mem_deref(st->device);
18 |
19 | list_unlink(&st->le);
20 | }
21 |
22 |
23 | static int pktsrc_alloc(struct vidsrc_st **stp, const struct vidsrc *vs,
24 | struct vidsrc_prm *prm, const struct vidsz *size,
25 | const char *fmt, const char *dev,
26 | vidsrc_frame_h *frameh, vidsrc_packet_h *packeth,
27 | vidsrc_error_h *errorh, void *arg)
28 | {
29 | struct vidsrc_st *st;
30 | int err;
31 | (void)fmt;
32 | (void)errorh;
33 | (void)vs;
34 |
35 | if (!stp || !prm || !size || !frameh)
36 | return EINVAL;
37 |
38 | st = mem_zalloc(sizeof(*st), pktsrc_deref);
39 | if (!st)
40 | return ENOMEM;
41 |
42 | st->packeth = packeth;
43 | st->frameh = frameh;
44 | st->arg = arg;
45 | st->fps = prm->fps;
46 |
47 | err = str_dup(&st->device, dev);
48 | if (err)
49 | goto out;
50 |
51 | mtx_lock(pktsrc.mtx);
52 | list_append(&pktsrc.srcl, &st->le, st);
53 | mtx_unlock(pktsrc.mtx);
54 |
55 | out:
56 | if (err)
57 | mem_deref(st);
58 | else
59 | *stp = st;
60 |
61 | return err;
62 | }
63 |
64 |
65 | static int pktsrc_thread(void *arg)
66 | {
67 | struct le *le;
68 | (void)arg;
69 |
70 | while (re_atomic_rlx(&pktsrc.run)) {
71 | sys_msleep(2);
72 |
73 | mtx_lock(pktsrc.mtx);
74 | LIST_FOREACH(&pktsrc.srcl, le)
75 | {
76 | struct vidsrc_st *st = le->data;
77 |
78 | vmix_codec_pkt(st);
79 | }
80 | mtx_unlock(pktsrc.mtx);
81 | }
82 |
83 | return 0;
84 | }
85 |
86 |
87 | int vmix_pktsrc_init(void)
88 | {
89 | int err;
90 |
91 | err = mutex_alloc(&pktsrc.mtx);
92 | if (err)
93 | return err;
94 |
95 | err = vidsrc_register(&pktsrc.vidsrc, baresip_vidsrcl(), "vmix_pktsrc",
96 | pktsrc_alloc, NULL);
97 | if (err)
98 | return err;
99 |
100 | re_atomic_rlx_set(&pktsrc.run, true);
101 |
102 | return thread_create_name(&pktsrc.thrd, "vmix_pktsrc", pktsrc_thread,
103 | NULL);
104 | }
105 |
106 |
107 | void vmix_pktsrc_close(void)
108 | {
109 | re_atomic_rlx_set(&pktsrc.run, false);
110 | thrd_join(pktsrc.thrd, NULL);
111 |
112 | pktsrc.vidsrc = mem_deref(pktsrc.vidsrc);
113 | pktsrc.mtx = mem_deref(pktsrc.mtx);
114 | }
115 |
--------------------------------------------------------------------------------
/modules/vmix/src.c:
--------------------------------------------------------------------------------
1 | /**
2 | * @file vidmix/src.c vidmix source
3 | *
4 | * Copyright (C) 2021-2022 Sebastian Reimers
5 | */
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include "vmix.h"
12 |
13 | static struct vidpacket dummy_vp = {.timestamp = 0};
14 | static mtx_t *vmix_mutex;
15 |
16 |
17 | static inline void vmix_lock(void)
18 | {
19 | mtx_lock(vmix_mutex);
20 | }
21 |
22 |
23 | static inline void vmix_unlock(void)
24 | {
25 | mtx_unlock(vmix_mutex);
26 | }
27 |
28 |
29 | /* delegate main src thread */
30 | static void vmix_delegate(void)
31 | {
32 | struct vidsrc_st *st;
33 |
34 | vmix_lock();
35 | if (!vmix_srcl.head)
36 | goto out;
37 |
38 | st = vmix_srcl.head->data;
39 | if (!st)
40 | goto out;
41 | vmix_unlock();
42 |
43 | vidmix_source_start(st->vidmix_src);
44 | re_atomic_rlx_set(&st->run, true);
45 |
46 | return;
47 | out:
48 | vmix_unlock();
49 | }
50 |
51 |
52 | static void destructor(void *arg)
53 | {
54 | struct vidsrc_st *st = arg;
55 | bool delegate = vmix_srcl.head == &st->le;
56 |
57 | re_atomic_rlx_set(&st->run, false);
58 |
59 | if (st->vidisp)
60 | st->vidisp->vidsrc = NULL;
61 |
62 | list_unlink(&st->he);
63 | mem_deref(st->device);
64 |
65 | vidmix_source_enable(st->vidmix_src, false);
66 | vidmix_source_stop(st->vidmix_src);
67 | vmix_lock();
68 | list_unlink(&st->le);
69 | vmix_unlock();
70 | mem_deref(st->vidmix_src);
71 |
72 | if (delegate)
73 | vmix_delegate();
74 | }
75 |
76 |
77 | static void frame_handler(uint64_t ts, const struct vidframe *frame, void *arg)
78 | {
79 | struct vidsrc_st *st = arg;
80 | struct le *le;
81 |
82 | if (!st || !st->frameh)
83 | return;
84 |
85 | if (!re_atomic_rlx(&st->run))
86 | return;
87 |
88 | st->frameh((struct vidframe *)frame, ts, st->arg);
89 |
90 | dummy_vp.keyframe = vmix_last_keyframe();
91 |
92 | vmix_lock();
93 | le = vmix_srcl.head;
94 | while (le) {
95 | st = le->data;
96 | if (!st)
97 | break;
98 |
99 | /* wait until keyframe arrive if src is not running */
100 | if (!re_atomic_rlx(&st->run) && !dummy_vp.keyframe) {
101 | le = le->next;
102 | continue;
103 | }
104 |
105 | re_atomic_rlx_set(&st->run, true);
106 | st->packeth(&dummy_vp, st->arg);
107 | le = le->next;
108 | }
109 | vmix_unlock();
110 |
111 | vmix_encode_flush();
112 |
113 | vmix_record(frame, ts);
114 | }
115 |
116 |
117 | int vmix_src_alloc(struct vidsrc_st **stp, const struct vidsrc *vs,
118 | struct vidsrc_prm *prm, const struct vidsz *size,
119 | const char *fmt, const char *dev, vidsrc_frame_h *frameh,
120 | vidsrc_packet_h *packeth, vidsrc_error_h *errorh, void *arg)
121 | {
122 | struct vidsrc_st *st;
123 | int err;
124 | (void)fmt;
125 | (void)errorh;
126 | (void)vs;
127 |
128 | if (!stp || !prm || !size || !frameh)
129 | return EINVAL;
130 |
131 | st = mem_zalloc(sizeof(*st), destructor);
132 | if (!st)
133 | return ENOMEM;
134 |
135 | st->packeth = packeth;
136 | st->frameh = frameh;
137 | st->arg = arg;
138 | st->fps = prm->fps;
139 |
140 | err = str_dup(&st->device, dev);
141 | if (err)
142 | goto out;
143 |
144 | err = vidmix_source_alloc(&st->vidmix_src, vmix_mix, size, st->fps,
145 | false, frame_handler, st);
146 | if (err)
147 | goto out;
148 |
149 | /* find a vidisp device with same name */
150 | st->vidisp = vmix_disp_find(dev);
151 | if (st->vidisp) {
152 | st->vidisp->vidsrc = st;
153 | }
154 |
155 | info("vidmix: src_alloc (%f fps)\n", st->fps);
156 | hash_append(vmix_src, hash_joaat_str(dev), &st->he, st);
157 |
158 | vmix_lock();
159 | list_append(&vmix_srcl, &st->le, st);
160 | vmix_unlock();
161 |
162 | vidmix_source_toggle_selfview(st->vidmix_src);
163 |
164 | vmix_request_keyframe();
165 |
166 | /* only start once */
167 | if (vmix_srcl.head == &st->le) {
168 | vidmix_source_start(st->vidmix_src);
169 | re_atomic_rlx_set(&st->run, true);
170 | }
171 |
172 | out:
173 | if (err)
174 | mem_deref(st);
175 | else
176 | *stp = st;
177 |
178 | return err;
179 | }
180 |
181 |
182 | static bool list_apply_handler(struct le *le, void *arg)
183 | {
184 | struct vidsrc_st *st = le->data;
185 |
186 | return 0 == str_cmp(st->device, arg);
187 | }
188 |
189 |
190 | struct vidsrc_st *vmix_src_find(const char *device)
191 | {
192 | return list_ledata(hash_lookup(vmix_src, hash_joaat_str(device),
193 | list_apply_handler, (void *)device));
194 | }
195 |
196 |
197 | void vmix_src_input(struct vidsrc_st *st, const struct vidframe *frame,
198 | uint64_t timestamp)
199 | {
200 | (void)timestamp;
201 |
202 | if (!st || !frame)
203 | return;
204 |
205 | vidmix_source_put(st->vidmix_src, frame);
206 | }
207 |
208 |
209 | int vmix_src_init(void)
210 | {
211 | return mutex_alloc(&vmix_mutex);
212 | }
213 |
214 |
215 | void vmix_src_close(void)
216 | {
217 | vmix_mutex = mem_deref(vmix_mutex);
218 | }
219 |
--------------------------------------------------------------------------------
/modules/vmix/vmix.c:
--------------------------------------------------------------------------------
1 | /**
2 | * @file vidmix.c Video bridge
3 | *
4 | * Copyright (C) 2022 Sebastian Reimers
5 | */
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include "vmix.h"
12 |
13 |
14 | /**
15 | * @defgroup vidmix vidmix
16 | *
17 | * Video bridge module
18 | *
19 | * This module can be used to connect two video devices together,
20 | * so that all output to VIDISP device is bridged as the input to
21 | * a VIDSRC device.
22 | *
23 | * Sample config:
24 | *
25 | \verbatim
26 | video_display vidmix,pseudo0
27 | video_source vidmix,pseudo0
28 | \endverbatim
29 | */
30 |
31 |
32 | static struct vidisp *vidisp;
33 | static struct vidsrc *vidsrc;
34 |
35 | struct hash *vmix_src;
36 | struct list vmix_srcl;
37 | struct hash *vmix_disp;
38 |
39 | struct vidmix *vmix_mix;
40 |
41 |
42 | /*
43 | * Relay UA events as publish messages to the Broker
44 | */
45 | static void ua_event_handler(enum ua_event ev, struct bevent *event, void *arg)
46 | {
47 | struct pl r, module = pl_null, myevent = pl_null, sess_id = pl_null;
48 | struct vidsrc_st *st;
49 | struct le *le;
50 | char device[64];
51 | static uint64_t last_modified;
52 | uint64_t now;
53 | (void)arg;
54 |
55 | const char *prm = bevent_get_text(event);
56 |
57 | if (ev != UA_EVENT_MODULE)
58 | return;
59 |
60 | /* disabled */
61 | return;
62 |
63 | now = tmr_jiffies();
64 |
65 | if ((now - last_modified) < 500)
66 | return;
67 |
68 | /* "aumix,talk,11_audio" */
69 | pl_set_str(&r, prm);
70 | re_regex(r.p, r.l, "[^,]+,[^,]+,[^_]+_[~]*", &module, &myevent,
71 | &sess_id, NULL);
72 |
73 | if (pl_strcmp(&module, "aumix"))
74 | return;
75 |
76 | if (pl_strcmp(&myevent, "talk"))
77 | return;
78 |
79 | re_snprintf(device, sizeof(device), "%r_video", &sess_id);
80 |
81 | st = vmix_src_find(device);
82 |
83 | if (!st)
84 | return;
85 |
86 | LIST_FOREACH(&vmix_srcl, le)
87 | {
88 | struct vidsrc_st *vst = le->data;
89 | vidmix_source_set_focus(vst->vidmix_src, st->vidmix_src, true);
90 | warning("set_focus: %s\n", device);
91 | }
92 |
93 | last_modified = tmr_jiffies();
94 | }
95 |
96 |
97 | static int video_rec_h(const char *folder, bool enable)
98 | {
99 | if (folder && enable) {
100 | return vmix_record_start(folder);
101 | }
102 |
103 | return vmix_record_close();
104 | }
105 |
106 |
107 | static uint32_t disp_enable_h(const char *device, bool enable)
108 | {
109 | struct vidsrc_st *src;
110 |
111 | if (!device)
112 | return 0;
113 |
114 | src = vmix_src_find(device);
115 | if (!src)
116 | return 0;
117 |
118 | vidmix_source_enable(src->vidmix_src, enable);
119 |
120 | return vidmix_source_get_pidx(src->vidmix_src);
121 | }
122 |
123 |
124 | void vmix_disp_focus(const char *device);
125 | void vmix_disp_focus(const char *device)
126 | {
127 | struct vidsrc_st *src;
128 | struct vidsrc_st *main_src;
129 |
130 | src = vmix_src_find(device);
131 | if (!src)
132 | return;
133 |
134 | main_src = vmix_srcl.head->data;
135 | if (!main_src)
136 | return;
137 |
138 | vidmix_source_set_focus(main_src->vidmix_src, src->vidmix_src, true);
139 | }
140 |
141 |
142 | void vmix_disp_solo(const char *device);
143 | void vmix_disp_solo(const char *device)
144 | {
145 | struct vidsrc_st *src;
146 | struct le *le;
147 |
148 | src = vmix_src_find(device);
149 | if (!src)
150 | return;
151 |
152 | LIST_FOREACH(&vmix_srcl, le)
153 | {
154 | struct vidsrc_st *srce = le->data;
155 |
156 | if (srce == src)
157 | vidmix_source_enable(srce->vidmix_src, true);
158 | else
159 | vidmix_source_enable(srce->vidmix_src, false);
160 | }
161 | }
162 |
163 |
164 | static int module_init(void)
165 | {
166 | int err;
167 |
168 | err = hash_alloc(&vmix_src, 32);
169 | err |= hash_alloc(&vmix_disp, 32);
170 | IF_ERR_GOTO_OUT(err);
171 |
172 | list_init(&vmix_srcl);
173 |
174 | err = vmix_codec_init();
175 | IF_ERR_GOTO_OUT(err);
176 |
177 | err = vmix_src_init();
178 | IF_ERR_GOTO_OUT(err);
179 |
180 | err = vidisp_register(&vidisp, baresip_vidispl(), "vmix",
181 | vmix_disp_alloc, NULL, vmix_disp_display, 0);
182 | IF_ERR_GOTO_OUT(err);
183 |
184 | err = vidsrc_register(&vidsrc, baresip_vidsrcl(), "vmix",
185 | vmix_src_alloc, NULL);
186 | IF_ERR_GOTO_OUT(err);
187 |
188 | err = vmix_pktsrc_init();
189 | IF_ERR_GOTO_OUT(err);
190 |
191 | err = vidmix_alloc(&vmix_mix);
192 | IF_ERR_GOTO_OUT(err);
193 |
194 | err = bevent_register(ua_event_handler, NULL);
195 |
196 | slmix_set_video_rec_h(slmix(), video_rec_h);
197 | slmix_set_video_disp_h(slmix(), disp_enable_h);
198 | out:
199 | return err;
200 | }
201 |
202 |
203 | static int module_close(void)
204 | {
205 | vmix_record_close();
206 | list_flush(&vmix_srcl);
207 | bevent_unregister(ua_event_handler);
208 | vidsrc = mem_deref(vidsrc);
209 | vidisp = mem_deref(vidisp);
210 |
211 | vmix_src = mem_deref(vmix_src);
212 | vmix_disp = mem_deref(vmix_disp);
213 |
214 | vmix_mix = mem_deref(vmix_mix);
215 |
216 | vmix_src_close();
217 | vmix_pktsrc_close();
218 | vmix_codec_close();
219 |
220 | return 0;
221 | }
222 |
223 |
224 | EXPORT_SYM const struct mod_export DECL_EXPORTS(vmix) = {
225 | "vmix",
226 | "video",
227 | module_init,
228 | module_close,
229 | };
230 |
--------------------------------------------------------------------------------
/modules/vmix/vmix.h:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | /**
4 | * @file vidmix.h vidmix -- internal interface
5 | *
6 | * Copyright (C) 2021 Sebastian Reimers
7 | */
8 |
9 |
10 | struct vidsrc_st {
11 | struct le he;
12 | struct le le;
13 | struct vidisp_st *vidisp;
14 | double fps;
15 | char *device;
16 | vidsrc_packet_h *packeth;
17 | vidsrc_frame_h *frameh;
18 | struct vidmix_source *vidmix_src;
19 | void *arg;
20 | uint64_t last_pkt;
21 | RE_ATOMIC bool run;
22 | };
23 |
24 |
25 | struct vidisp_st {
26 | struct le le;
27 | struct vidsrc_st *vidsrc;
28 | char *device;
29 | };
30 |
31 |
32 | extern struct hash *vmix_src;
33 | extern struct list vmix_srcl;
34 | extern struct hash *vmix_disp;
35 | extern struct vidmix *vmix_mix;
36 |
37 |
38 | int vmix_disp_alloc(struct vidisp_st **stp, const struct vidisp *vd,
39 | struct vidisp_prm *prm, const char *dev,
40 | vidisp_resize_h *resizeh, void *arg);
41 | int vmix_disp_display(struct vidisp_st *st, const char *title,
42 | const struct vidframe *frame, uint64_t timestamp);
43 | struct vidisp_st *vmix_disp_find(const char *device);
44 |
45 |
46 | int vmix_src_init(void);
47 | void vmix_src_close(void);
48 | int vmix_src_alloc(struct vidsrc_st **stp, const struct vidsrc *vs,
49 | struct vidsrc_prm *prm, const struct vidsz *size,
50 | const char *fmt, const char *dev, vidsrc_frame_h *frameh,
51 | vidsrc_packet_h *packeth, vidsrc_error_h *errorh,
52 | void *arg);
53 | struct vidsrc_st *vmix_src_find(const char *device);
54 | void vmix_src_input(struct vidsrc_st *st, const struct vidframe *frame,
55 | uint64_t timestamp);
56 |
57 |
58 | int vmix_record_start(const char *record_folder);
59 | int vmix_record(const struct vidframe *frame, uint64_t ts);
60 | int vmix_record_close(void);
61 |
62 | void vmix_codec_pkt(struct vidsrc_st *st);
63 | int vmix_codec_init(void);
64 | void vmix_codec_close(void);
65 | bool vmix_last_keyframe(void);
66 | void vmix_request_keyframe(void);
67 | void vmix_encode_flush(void);
68 |
69 | int vmix_pktsrc_init(void);
70 | void vmix_pktsrc_close(void);
71 |
--------------------------------------------------------------------------------
/patches/2636.patch:
--------------------------------------------------------------------------------
1 | From 455cde5086527ebdceaea9ca01b7a2ea95f93d34 Mon Sep 17 00:00:00 2001
2 | From: Sebastian Reimers
3 | Date: Sun, 25 Jun 2023 11:09:27 +0200
4 | Subject: [PATCH] webrtc: add media track sdp direction
5 |
6 | ---
7 | include/baresip.h | 12 ++++++------
8 | src/mediatrack.c | 44 ++++++++++++++++++++++++++++----------------
9 | src/peerconn.c | 8 ++++++--
10 | webrtc/src/sess.c | 6 +++---
11 | 4 files changed, 43 insertions(+), 27 deletions(-)
12 |
13 | diff --git a/include/baresip.h b/include/baresip.h
14 | index 957e68a39..35d784432 100644
15 | --- a/include/baresip.h
16 | +++ b/include/baresip.h
17 | @@ -1701,12 +1701,12 @@ int peerconnection_new(struct peer_connection **pcp,
18 | peerconnection_gather_h *gatherh,
19 | peerconnection_estab_h,
20 | peerconnection_close_h *closeh, void *arg);
21 | -int peerconnection_add_audio_track(struct peer_connection *pc,
22 | - const struct config *cfg,
23 | - struct list *aucodecl);
24 | -int peerconnection_add_video_track(struct peer_connection *pc,
25 | - const struct config *cfg,
26 | - struct list *vidcodecl);
27 | +int peerconnection_add_audio_track(struct peer_connection *pc,
28 | + const struct config *cfg,
29 | + struct list *aucodecl, enum sdp_dir dir);
30 | +int peerconnection_add_video_track(struct peer_connection *pc,
31 | + const struct config *cfg,
32 | + struct list *vidcodecl, enum sdp_dir dir);
33 | int peerconnection_set_remote_descr(struct peer_connection *pc,
34 | const struct session_description *sd);
35 | int peerconnection_create_offer(struct peer_connection *sess,
36 | diff --git a/src/mediatrack.c b/src/mediatrack.c
37 | index 37dc5a771..46c9ac346 100644
38 | --- a/src/mediatrack.c
39 | +++ b/src/mediatrack.c
40 | @@ -58,8 +58,10 @@ int mediatrack_start_audio(struct media_track *media,
41 |
42 | info("mediatrack: start audio\n");
43 |
44 | - fmt = sdp_media_rformat(stream_sdpmedia(audio_strm(au)), NULL);
45 | - if (fmt) {
46 | + struct sdp_media *sdpm = stream_sdpmedia(audio_strm(au));
47 | + fmt = sdp_media_rformat(sdpm, NULL);
48 | +
49 | + if (fmt && sdp_media_dir(sdpm) & SDP_SENDONLY) {
50 | struct aucodec *ac = fmt->data;
51 |
52 | err = audio_encoder_set(au, ac, fmt->pt, fmt->params);
53 | @@ -102,34 +104,44 @@ int mediatrack_start_video(struct media_track *media)
54 |
55 | info("mediatrack: start video\n");
56 |
57 | - fmt = sdp_media_rformat(stream_sdpmedia(video_strm(vid)), NULL);
58 | - if (fmt) {
59 | - struct vidcodec *vc = fmt->data;
60 | + struct sdp_media *sdpm = stream_sdpmedia(video_strm(vid));
61 | + enum sdp_dir dir = sdp_media_dir(sdpm);
62 |
63 | - err = video_encoder_set(vid, vc, fmt->pt, fmt->params);
64 | - if (err) {
65 | - warning("mediatrack: start:"
66 | - " video_encoder_set error: %m\n", err);
67 | - return err;
68 | - }
69 | + fmt = sdp_media_rformat(sdpm, NULL);
70 | + if (!fmt) {
71 | + info("mediatrack: video stream is disabled..\n");
72 | + return 0;
73 | + }
74 |
75 | + struct vidcodec *vc = fmt->data;
76 | +
77 | + err = video_encoder_set(vid, vc, fmt->pt, fmt->params);
78 | + if (err) {
79 | + warning("mediatrack: start:"
80 | + " video_encoder_set error: %m\n",
81 | + err);
82 | + return err;
83 | + }
84 | +
85 | + if (dir & SDP_SENDONLY) {
86 | err = video_start_source(vid);
87 | if (err) {
88 | warning("mediatrack: start:"
89 | - " video_start_source error: %m\n", err);
90 | + " video_start_source error: %m\n",
91 | + err);
92 | return err;
93 | }
94 | + }
95 |
96 | + if (dir & SDP_RECVONLY) {
97 | err = video_start_display(vid, "webrtc");
98 | if (err) {
99 | warning("mediatrack: start:"
100 | - " video_start_display error: %m\n", err);
101 | + " video_start_display error: %m\n",
102 | + err);
103 | return err;
104 | }
105 | }
106 | - else {
107 | - info("mediatrack: video stream is disabled..\n");
108 | - }
109 |
110 | stream_set_rtcp_interval(video_strm(vid), 1000);
111 |
112 | diff --git a/src/peerconn.c b/src/peerconn.c
113 | index 8e24fc454..41b2b60b4 100644
114 | --- a/src/peerconn.c
115 | +++ b/src/peerconn.c
116 | @@ -307,7 +307,7 @@ static void mediatrack_close_handler(int err, void *arg)
117 | */
118 | int peerconnection_add_audio_track(struct peer_connection *pc,
119 | const struct config *cfg,
120 | - struct list *aucodecl)
121 | + struct list *aucodecl, enum sdp_dir dir)
122 | {
123 | struct media_track *media;
124 | bool offerer;
125 | @@ -332,6 +332,8 @@ int peerconnection_add_audio_track(struct peer_connection *pc,
126 | return err;
127 | }
128 |
129 | + stream_set_ldir(media_get_stream(media), dir);
130 | +
131 | mediatrack_set_handlers(media);
132 |
133 | return 0;
134 | @@ -343,7 +345,7 @@ int peerconnection_add_audio_track(struct peer_connection *pc,
135 | */
136 | int peerconnection_add_video_track(struct peer_connection *pc,
137 | const struct config *cfg,
138 | - struct list *vidcodecl)
139 | + struct list *vidcodecl, enum sdp_dir dir)
140 | {
141 | struct media_track *media;
142 | bool offerer;
143 | @@ -373,6 +375,8 @@ int peerconnection_add_video_track(struct peer_connection *pc,
144 | return err;
145 | }
146 |
147 | + stream_set_ldir(media_get_stream(media), dir);
148 | +
149 | mediatrack_set_handlers(media);
150 |
151 | return 0;
152 | diff --git a/webrtc/src/sess.c b/webrtc/src/sess.c
153 | index 15aa15dd1..78308b7a4 100644
154 | --- a/webrtc/src/sess.c
155 | +++ b/webrtc/src/sess.c
156 | @@ -152,14 +152,14 @@ int session_start(struct session *sess,
157 | }
158 |
159 | err = peerconnection_add_audio_track(sess->pc, config,
160 | - baresip_aucodecl());
161 | + baresip_aucodecl(), SDP_SENDRECV);
162 | if (err) {
163 | warning("demo: add_audio failed (%m)\n", err);
164 | return err;
165 | }
166 |
167 | - err = peerconnection_add_video_track(sess->pc, config,
168 | - baresip_vidcodecl());
169 | + err = peerconnection_add_video_track(
170 | + sess->pc, config, baresip_vidcodecl(), SDP_SENDRECV);
171 | if (err) {
172 | warning("demo: add_video failed (%m)\n", err);
173 | return err;
174 |
--------------------------------------------------------------------------------
/patches/avcodec_decode_scale_crash.patch:
--------------------------------------------------------------------------------
1 | diff --git a/modules/avcodec/decode.c b/modules/avcodec/decode.c
2 | index fe58d161..60079b82 100644
3 | --- a/modules/avcodec/decode.c
4 | +++ b/modules/avcodec/decode.c
5 | @@ -12,6 +12,8 @@
6 | #include
7 | #include
8 | #include
9 | +#include
10 | +#include
11 | #include "h26x.h"
12 | #include "avcodec.h"
13 |
14 | @@ -142,6 +144,11 @@ static int init_decoder(struct viddec_state *st, const char *name)
15 | st->ctx->hw_device_ctx = av_buffer_ref(avcodec_hw_device_ctx);
16 | st->ctx->get_format = get_hw_format;
17 |
18 | + int ret = av_image_alloc(st->pict->data, st->pict->linesize,
19 | + 1920, 1080, AV_PIX_FMT_YUV420P, 32);
20 | + if (ret < 0)
21 | + return ENOMEM;
22 | +
23 | info("avcodec: decode: hardware accel enabled (%s)\n",
24 | av_hwdevice_get_type_name(avcodec_hw_type));
25 | }
26 | @@ -152,6 +159,7 @@ static int init_decoder(struct viddec_state *st, const char *name)
27 | if (avcodec_open2(st->ctx, st->codec, NULL) < 0)
28 | return ENOENT;
29 |
30 | +
31 | return 0;
32 | }
33 |
34 | @@ -202,6 +210,8 @@ static int ffdecode(struct viddec_state *st, struct vidframe *frame,
35 | bool *intra)
36 | {
37 | AVFrame *hw_frame = NULL;
38 | + AVFrame *tmp_frame = NULL;
39 | + struct SwsContext* scaler = NULL;
40 | AVPacket *avpkt;
41 | int i, got_picture, ret;
42 | int err = 0;
43 | @@ -210,6 +220,9 @@ static int ffdecode(struct viddec_state *st, struct vidframe *frame,
44 | hw_frame = av_frame_alloc();
45 | if (!hw_frame)
46 | return ENOMEM;
47 | + tmp_frame = av_frame_alloc();
48 | + if (!tmp_frame)
49 | + return ENOMEM;
50 | }
51 |
52 | err = mbuf_fill(st->mb, 0x00, AV_INPUT_BUFFER_PADDING_SIZE);
53 | @@ -245,13 +258,15 @@ static int ffdecode(struct viddec_state *st, struct vidframe *frame,
54 | goto out;
55 | }
56 |
57 | - got_picture = true;
58 | + got_picture = (ret == 0);
59 |
60 | if (got_picture) {
61 |
62 | if (hw_frame) {
63 | /* retrieve data from GPU to CPU */
64 | - ret = av_hwframe_transfer_data(st->pict, hw_frame, 0);
65 | + /* use tmp_frame dest to avoid crashes (unclear if
66 | + * this the root cause!) */
67 | + ret = av_hwframe_transfer_data(tmp_frame, hw_frame, 0);
68 | if (ret < 0) {
69 | warning("avcodec: decode: Error transferring"
70 | " the data to system memory\n");
71 | @@ -259,21 +274,44 @@ static int ffdecode(struct viddec_state *st, struct vidframe *frame,
72 | }
73 |
74 | st->pict->key_frame = hw_frame->key_frame;
75 | - }
76 |
77 | - frame->fmt = avpixfmt_to_vidfmt(st->pict->format);
78 | - if (frame->fmt == (enum vidfmt)-1) {
79 | - warning("avcodec: decode: bad pixel format"
80 | - " (%i) (%s)\n",
81 | - st->pict->format,
82 | - av_get_pix_fmt_name(st->pict->format));
83 | - goto out;
84 | + scaler = sws_getContext(
85 | + st->ctx->width, st->ctx->height,
86 | + tmp_frame->format, st->ctx->width,
87 | + st->ctx->height, AV_PIX_FMT_YUV420P,
88 | + SWS_BICUBIC, NULL, NULL, NULL);
89 | + if (!scaler) {
90 | + warning("avcodec: sws_ctx: Error\n");
91 | + goto out;
92 | + }
93 | +
94 | + ret = sws_scale(scaler,
95 | + (const uint8_t *const *)tmp_frame->data,
96 | + tmp_frame->linesize, 0, st->ctx->height,
97 | + st->pict->data, st->pict->linesize);
98 | + if (ret < 0) {
99 | + warning("avcodec: sws_scale: Error\n");
100 | + goto out;
101 | + }
102 | +
103 | + frame->fmt = VID_FMT_YUV420P;
104 | + }
105 | + else {
106 | + frame->fmt = avpixfmt_to_vidfmt(st->pict->format);
107 | + if (frame->fmt == (enum vidfmt) - 1) {
108 | + warning("avcodec: decode: bad pixel format"
109 | + " (%i) (%s)\n",
110 | + st->pict->format,
111 | + av_get_pix_fmt_name(st->pict->format));
112 | + goto out;
113 | + }
114 | }
115 |
116 | for (i=0; i<4; i++) {
117 | frame->data[i] = st->pict->data[i];
118 | frame->linesize[i] = st->pict->linesize[i];
119 | }
120 | +
121 | frame->size.w = st->ctx->width;
122 | frame->size.h = st->ctx->height;
123 |
124 | @@ -287,6 +325,8 @@ static int ffdecode(struct viddec_state *st, struct vidframe *frame,
125 |
126 | out:
127 | av_frame_free(&hw_frame);
128 | + av_frame_free(&tmp_frame);
129 | + sws_freeContext(scaler);
130 | av_packet_free(&avpkt);
131 | return err;
132 | }
133 |
--------------------------------------------------------------------------------
/patches/avcodec_encode_refs.patch:
--------------------------------------------------------------------------------
1 | diff --git a/modules/avcodec/encode.c b/modules/avcodec/encode.c
2 | index b45cd094..865d04cd 100644
3 | --- a/modules/avcodec/encode.c
4 | +++ b/modules/avcodec/encode.c
5 | @@ -181,6 +181,7 @@ static int open_encoder(struct videnc_state *st,
6 | st->ctx->time_base.num = 1;
7 | st->ctx->time_base.den = prm->fps;
8 | st->ctx->gop_size = keyint * prm->fps;
9 | + st->ctx->refs = 0;
10 |
11 | if (0 == str_cmp(st->codec->name, "libx264")) {
12 |
13 |
--------------------------------------------------------------------------------
/patches/baresip_2936.patch:
--------------------------------------------------------------------------------
1 | diff --git a/docs/examples/config b/docs/examples/config
2 | index 2b3549fc..5cfa3697 100644
3 | --- a/docs/examples/config
4 | +++ b/docs/examples/config
5 | @@ -265,6 +265,10 @@ video_selfview window # {window,pip}
6 | #avcodec_profile_level_id 42002a
7 | #avcodec_keyint 10 # keyframe interval in [sec]
8 |
9 | +# vp8
10 | +#vp8_enc_threads 1
11 | +#vp8_enc_cpuused 16 # Range -16..16, greater 0 increases speed over quality
12 | +
13 | # ctrl_dbus
14 | #ctrl_dbus_use system # system, session
15 |
16 | diff --git a/modules/vp8/encode.c b/modules/vp8/encode.c
17 | index e26fb90e..00cd2d6f 100644
18 | --- a/modules/vp8/encode.c
19 | +++ b/modules/vp8/encode.c
20 | @@ -15,6 +15,7 @@
21 |
22 | enum {
23 | HDR_SIZE = 4,
24 | + KEYFRAME_INTERVAL = 10 /* Keyframes per second */
25 | };
26 |
27 |
28 | @@ -92,11 +93,17 @@ static int open_encoder(struct videnc_state *ves, const struct vidsz *size)
29 | vpx_codec_enc_cfg_t cfg;
30 | vpx_codec_err_t res;
31 | vpx_codec_flags_t flags = 0;
32 | + uint32_t threads = 1;
33 | + int32_t cpuused = 16;
34 |
35 | res = vpx_codec_enc_config_default(&vpx_codec_vp8_cx_algo, &cfg, 0);
36 | if (res)
37 | return EPROTO;
38 |
39 | + conf_get_u32(conf_cur(), "vp8_enc_threads", &threads);
40 | + conf_get_i32(conf_cur(), "vp8_enc_cpuused", &cpuused);
41 | +
42 | + cfg.g_threads = threads;
43 | cfg.g_profile = 2;
44 | cfg.g_w = size->w;
45 | cfg.g_h = size->h;
46 | @@ -105,11 +112,16 @@ static int open_encoder(struct videnc_state *ves, const struct vidsz *size)
47 | #ifdef VPX_ERROR_RESILIENT_DEFAULT
48 | cfg.g_error_resilient = VPX_ERROR_RESILIENT_DEFAULT;
49 | #endif
50 | - cfg.g_pass = VPX_RC_ONE_PASS;
51 | - cfg.g_lag_in_frames = 0;
52 | - cfg.rc_end_usage = VPX_VBR;
53 | - cfg.rc_target_bitrate = ves->bitrate;
54 | - cfg.kf_mode = VPX_KF_AUTO;
55 | + cfg.g_pass = VPX_RC_ONE_PASS;
56 | + cfg.g_lag_in_frames = 0;
57 | + cfg.rc_end_usage = VPX_CBR;
58 | + cfg.rc_target_bitrate = ves->bitrate / 1000; /* kbps */
59 | + cfg.rc_overshoot_pct = 15;
60 | + cfg.rc_undershoot_pct = 100;
61 | + cfg.rc_dropframe_thresh = 0;
62 | + cfg.kf_mode = VPX_KF_AUTO;
63 | + cfg.kf_min_dist = ves->fps * KEYFRAME_INTERVAL;
64 | + cfg.kf_max_dist = ves->fps * KEYFRAME_INTERVAL;
65 |
66 | if (ves->ctxup) {
67 | debug("vp8: re-opening encoder\n");
68 | @@ -130,7 +142,7 @@ static int open_encoder(struct videnc_state *ves, const struct vidsz *size)
69 |
70 | ves->ctxup = true;
71 |
72 | - res = vpx_codec_control(&ves->ctx, VP8E_SET_CPUUSED, 16);
73 | + res = vpx_codec_control(&ves->ctx, VP8E_SET_CPUUSED, cpuused);
74 | if (res) {
75 | warning("vp8: codec ctrl: %s\n", vpx_codec_err_to_string(res));
76 | }
77 | diff --git a/modules/vp8/vp8.c b/modules/vp8/vp8.c
78 | index 10b06f84..75d9b706 100644
79 | --- a/modules/vp8/vp8.c
80 | +++ b/modules/vp8/vp8.c
81 | @@ -35,7 +35,7 @@ static struct vp8_vidcodec vp8 = {
82 | .fmtp_ench = vp8_fmtp_enc,
83 | .packetizeh = vp8_encode_packetize,
84 | },
85 | - .max_fs = 3600,
86 | + .max_fs = 8100, /* 1920 x 1080 / (16^2) */
87 | };
88 |
89 |
90 | diff --git a/src/config.c b/src/config.c
91 | index 9b8698e7..31e43a14 100644
92 | --- a/src/config.c
93 | +++ b/src/config.c
94 | @@ -1299,6 +1299,14 @@ int config_write_template(const char *file, const struct config *cfg)
95 | default_avcodec_hwaccel()
96 | );
97 |
98 | + (void)re_fprintf(f,
99 | + "# vp8\n"
100 | + "#vp8_enc_threads 1\n"
101 | + "#vp8_enc_cpuused 16"
102 | + " # range -16..16,"
103 | + " greater 0 increases speed over quality\n"
104 | + );
105 | +
106 | (void)re_fprintf(f,
107 | "\n# ctrl_dbus\n"
108 | "#ctrl_dbus_use\tsystem\t\t# system, session\n");
109 |
--------------------------------------------------------------------------------
/patches/baresip_ice_sdp_mdns.patch:
--------------------------------------------------------------------------------
1 | diff --git a/modules/ice/ice.c b/modules/ice/ice.c
2 | index 3c9d3970..467d4ca1 100644
3 | --- a/modules/ice/ice.c
4 | +++ b/modules/ice/ice.c
5 | @@ -35,6 +35,7 @@ struct mnat_sess {
6 | struct stun_dns *dnsq;
7 | struct sdp_session *sdp;
8 | struct tmr tmr_async;
9 | + struct tmr tmr_async_sdp;
10 | char lufrag[8];
11 | char lpwd[32];
12 | uint64_t tiebrk;
13 | @@ -312,6 +313,7 @@ static void session_destructor(void *arg)
14 | struct mnat_sess *sess = arg;
15 |
16 | tmr_cancel(&sess->tmr_async);
17 | + tmr_cancel(&sess->tmr_async_sdp);
18 | list_flush(&sess->medial);
19 | mem_deref(sess->dnsq);
20 | mem_deref(sess->user);
21 | @@ -485,6 +487,24 @@ static void tmr_async_handler(void *arg)
22 | }
23 |
24 |
25 | +static void tmr_async_sdp_handler(void *arg)
26 | +{
27 | + struct mnat_sess *sess = arg;
28 | + struct le *le;
29 | + LIST_FOREACH(&sess->medial, le)
30 | + {
31 | + struct mnat_media *m = le->data;
32 | +
33 | + if (!sdp_media_has_media(m->sdpm))
34 | + continue;
35 | +
36 | + /* start ice if we have remote candidates */
37 | + if (!list_isempty(icem_rcandl(m->icem)))
38 | + icem_conncheck_start(m->icem);
39 | + }
40 | +}
41 | +
42 | +
43 | static int session_alloc(struct mnat_sess **sessp,
44 | const struct mnat *mnat, struct dnsc *dnsc,
45 | int af, const struct stun_uri *srv,
46 | @@ -797,14 +817,6 @@ static int ice_start(struct mnat_sess *sess)
47 | if (sdp_media_has_media(m->sdpm)) {
48 | m->complete = false;
49 |
50 | - /* start ice if we have remote candidates */
51 | - if (!list_isempty(icem_rcandl(m->icem))) {
52 | -
53 | - err = icem_conncheck_start(m->icem);
54 | - if (err)
55 | - return err;
56 | - }
57 | -
58 | /* set the pair states
59 | -- first media stream only */
60 | if (sess->medial.head == le) {
61 | @@ -816,6 +828,9 @@ static int ice_start(struct mnat_sess *sess)
62 | }
63 | }
64 |
65 | + /* @TODO: detect if sdp is ready */
66 | + tmr_start(&sess->tmr_async_sdp, 10, tmr_async_sdp_handler, sess);
67 | +
68 | sess->started = true;
69 |
70 | return 0;
71 |
--------------------------------------------------------------------------------
/patches/baresip_jbuf_nack.patch:
--------------------------------------------------------------------------------
1 | diff --git a/include/baresip.h b/include/baresip.h
2 | index a56768f8..2228862b 100644
3 | --- a/include/baresip.h
4 | +++ b/include/baresip.h
5 | @@ -1547,6 +1547,7 @@ struct jbuf_stat {
6 | int jbuf_alloc(struct jbuf **jbp, uint32_t mind, uint32_t maxd,
7 | uint32_t maxsz);
8 | void jbuf_set_srate(struct jbuf *jb, uint32_t srate);
9 | +void jbuf_set_socket(struct jbuf *jb, struct rtp_sock *rtp);
10 | void jbuf_set_id(struct jbuf *jb, struct pl *id);
11 | int jbuf_set_type(struct jbuf *jb, enum jbuf_type jbtype);
12 | int jbuf_put(struct jbuf *jb, const struct rtp_header *hdr, void *mem);
13 | diff --git a/src/jbuf.c b/src/jbuf.c
14 | index 963acf59..298637bd 100644
15 | --- a/src/jbuf.c
16 | +++ b/src/jbuf.c
17 | @@ -62,6 +62,7 @@ struct packet {
18 | */
19 | struct jbuf {
20 | struct pl *id; /**< Jitter buffer Identifier */
21 | + struct rtp_sock *rtp;/**< RTP Socket (Enables RTCP NACK sending) */
22 | struct list pooll; /**< List of free packets in pool */
23 | struct list packetl; /**< List of buffered packets */
24 | uint32_t n; /**< [# packets] Current # of packets in buffer */
25 | @@ -251,6 +252,23 @@ out:
26 | }
27 |
28 |
29 | +/**
30 | + * Set rtp socket for RTCP NACK handling
31 | + *
32 | + * @param jb The jitter buffer.
33 | + * @param rtp RTP Socket
34 | + */
35 | +void jbuf_set_socket(struct jbuf *jb, struct rtp_sock *rtp)
36 | +{
37 | + if (!jb)
38 | + return;
39 | +
40 | + mtx_lock(jb->lock);
41 | + jb->rtp = rtp;
42 | + mtx_unlock(jb->lock);
43 | +}
44 | +
45 | +
46 | /**
47 | * Set jitter samplerate (clockrate).
48 | *
49 | @@ -464,6 +482,23 @@ static uint32_t calc_playout_time(struct jbuf *jb, struct packet *p)
50 | }
51 |
52 |
53 | +static inline void send_nack(struct jbuf *jb, uint16_t last_seq,
54 | + int16_t seq_diff)
55 | +{
56 | + uint16_t pid = last_seq + 1;
57 | + uint16_t blp = 0;
58 | +
59 | + for (int i = 0; i < seq_diff - 2; i++) {
60 | + blp |= (1 << i);
61 | + }
62 | +
63 | + warning("jbuf: RTCP_NACK missing: %u diff: %d blp: %02X\n", pid,
64 | + seq_diff, blp);
65 | +
66 | + rtcp_send_gnack(jb->rtp, jb->ssrc, pid, blp);
67 | +}
68 | +
69 | +
70 | /**
71 | * Put one packet into the jitter buffer
72 | *
73 | @@ -525,10 +560,21 @@ int jbuf_put(struct jbuf *jb, const struct rtp_header *hdr, void *mem)
74 |
75 | tail = jb->packetl.tail;
76 |
77 | - /* If buffer is empty -> append to tail
78 | - Frame is later than tail -> append to tail
79 | - */
80 | - if (!tail || seq_less(((struct packet *)tail->data)->hdr.seq, seq)) {
81 | + /* If buffer is empty -> append to tail */
82 | + if (!tail) {
83 | + list_append(&jb->packetl, &f->le, f);
84 | + goto success;
85 | + }
86 | +
87 | + uint16_t last_seq = ((struct packet *)tail->data)->hdr.seq;
88 | +
89 | + /* Frame is later than tail -> append to tail */
90 | + if (seq_less(last_seq, seq)) {
91 | + const int16_t seq_diff = seq - last_seq;
92 | +
93 | + if (jb->rtp && seq_diff > 1)
94 | + send_nack(jb, last_seq, seq_diff);
95 | +
96 | list_append(&jb->packetl, &f->le, f);
97 | goto success;
98 | }
99 | diff --git a/src/rtprecv.c b/src/rtprecv.c
100 | index dc95dc3f..b8d62b26 100644
101 | --- a/src/rtprecv.c
102 | +++ b/src/rtprecv.c
103 | @@ -535,6 +535,8 @@ void rtprecv_set_socket(struct rtp_receiver *rx, struct rtp_sock *rtp)
104 | {
105 | mtx_lock(rx->mtx);
106 | rx->rtp = rtp;
107 | + if (stream_type(rx->strm) == MEDIA_VIDEO)
108 | + jbuf_set_socket(rx->jbuf, rx->rtp);
109 | mtx_unlock(rx->mtx);
110 | }
111 |
112 |
--------------------------------------------------------------------------------
/patches/baresip_packet_dup_handler.patch:
--------------------------------------------------------------------------------
1 | diff --git a/modules/avcodec/encode.c b/modules/avcodec/encode.c
2 | index b45cd09..d9b4dd8 100644
3 | --- a/modules/avcodec/encode.c
4 | +++ b/modules/avcodec/encode.c
5 | @@ -385,6 +385,7 @@ int avcodec_encode_update(struct videnc_state **vesp,
6 | return err;
7 | }
8 |
9 | +int packet_dup_handler(uint64_t ts, uint8_t *buf, size_t size, bool keyframe);
10 |
11 | int avcodec_encode(struct videnc_state *st, bool update,
12 | const struct vidframe *frame, uint64_t timestamp)
13 | @@ -392,6 +393,7 @@ int avcodec_encode(struct videnc_state *st, bool update,
14 | AVFrame *pict = NULL;
15 | AVFrame *hw_frame = NULL;
16 | AVPacket *pkt = NULL;
17 | + static bool update2 = false;
18 | int i, err = 0, ret;
19 | uint64_t ts;
20 |
21 | @@ -443,10 +445,11 @@ int avcodec_encode(struct videnc_state *st, bool update,
22 | pict->linesize[i] = frame->linesize[i];
23 | }
24 |
25 | - if (update) {
26 | + if (update || update2) {
27 | debug("avcodec: encoder picture update\n");
28 | pict->key_frame = 1;
29 | pict->pict_type = AV_PICTURE_TYPE_I;
30 | + update2 = false;
31 | }
32 |
33 | pict->color_range = AVCOL_RANGE_MPEG;
34 | @@ -495,6 +498,10 @@ int avcodec_encode(struct videnc_state *st, bool update,
35 |
36 | ts = video_calc_rtp_timestamp_fix(pkt->pts);
37 |
38 | + ret = packet_dup_handler(timestamp, pkt->data, pkt->size, !!(pkt->flags & AV_PKT_FLAG_KEY));
39 | + if (ret)
40 | + update2 = true;
41 | +
42 | switch (st->codec_id) {
43 |
44 | case AV_CODEC_ID_H264:
45 |
--------------------------------------------------------------------------------
/patches/baresip_stream_enable.patch:
--------------------------------------------------------------------------------
1 | diff --git a/include/baresip.h b/include/baresip.h
2 | index 1510249f..508d9ea6 100644
3 | --- a/include/baresip.h
4 | +++ b/include/baresip.h
5 | @@ -1431,6 +1431,7 @@ const char *stream_peer(const struct stream *strm);
6 | int stream_bundle_init(struct stream *strm, bool offerer);
7 | int stream_debug(struct re_printf *pf, const struct stream *s);
8 | void stream_enable_rtp_timeout(struct stream *strm, uint32_t timeout_ms);
9 | +void stream_flush(struct stream *s);
10 |
11 |
12 | /*
13 | diff --git a/src/core.h b/src/core.h
14 | index 56fa4c19..914861d2 100644
15 | --- a/src/core.h
16 | +++ b/src/core.h
17 | @@ -299,7 +299,6 @@ int stream_resend(struct stream *s, uint16_t seq, bool ext, bool marker,
18 | int pt, uint32_t ts, struct mbuf *mb);
19 |
20 | /* Receive */
21 | -void stream_flush(struct stream *s);
22 | int stream_ssrc_rx(const struct stream *strm, uint32_t *ssrc);
23 |
24 |
25 | diff --git a/src/stream.c b/src/stream.c
26 | index 8e70f0ad..eead4303 100644
27 | --- a/src/stream.c
28 | +++ b/src/stream.c
29 | @@ -357,7 +357,7 @@ static void rtp_handler(const struct sa *src, const struct rtp_header *hdr,
30 |
31 | MAGIC_CHECK(s);
32 |
33 | - if (!s->rx.enabled && s->type == MEDIA_AUDIO)
34 | + if (!s->rx.enabled)
35 | return;
36 |
37 | if (rtp_pt_is_rtcp(hdr->pt)) {
38 | @@ -1177,8 +1177,7 @@ void stream_flush(struct stream *s)
39 | if (s->rx.jbuf)
40 | jbuf_flush(s->rx.jbuf);
41 |
42 | - if (s->type == MEDIA_AUDIO)
43 | - rtp_clear(s->rtp);
44 | + rtp_clear(s->rtp);
45 | }
46 |
47 |
48 |
--------------------------------------------------------------------------------
/patches/baresip_video_latency.patch:
--------------------------------------------------------------------------------
1 | diff --git a/src/video.c b/src/video.c
2 | index 58bcf53..fadafeb 100644
3 | --- a/src/video.c
4 | +++ b/src/video.c
5 | @@ -102,6 +102,7 @@ struct vtx {
6 | uint64_t ts_last; /**< Last RTP timestamp sent */
7 | thrd_t thrd; /**< Tx-Thread */
8 | RE_ATOMIC bool run; /**< Tx-Thread is active */
9 | + uint64_t ts_latency; /**< RTP timestamp latency */
10 |
11 | /** Statistics */
12 | struct {
13 | @@ -175,6 +176,7 @@ struct vidqent {
14 | bool marker;
15 | uint8_t pt;
16 | uint32_t ts;
17 | + uint64_t ts64;
18 | uint64_t jfs_nack;
19 | uint16_t seq;
20 | struct mbuf *mb;
21 | @@ -195,7 +197,7 @@ static void vidqent_destructor(void *arg)
22 |
23 |
24 | static int vidqent_alloc(struct vidqent **qentp, struct stream *strm,
25 | - bool marker, uint8_t pt, uint32_t ts,
26 | + bool marker, uint8_t pt, uint32_t ts, uint64_t ts64,
27 | const uint8_t *hdr, size_t hdr_len,
28 | const uint8_t *pld, size_t pld_len)
29 | {
30 | @@ -214,6 +216,7 @@ static int vidqent_alloc(struct vidqent **qentp, struct stream *strm,
31 | qent->marker = marker;
32 | qent->pt = pt;
33 | qent->ts = ts;
34 | + qent->ts64 = ts64;
35 |
36 | qent->mb = mbuf_alloc(RTP_PRESZ + hdr_len + pld_len + RTP_TRAILSZ);
37 | if (!qent->mb) {
38 | @@ -277,6 +280,7 @@ static void vidqueue_poll(struct vtx *vtx, uint64_t jfs, uint64_t prev_jfs)
39 | struct le *le;
40 | struct mbuf *mbd;
41 | uint64_t jfs_nack = jfs + NACK_QUEUE_TIME;
42 | + static uint64_t jfs_latency = 0;
43 |
44 | if (!vtx)
45 | return;
46 | @@ -296,6 +300,23 @@ static void vidqueue_poll(struct vtx *vtx, uint64_t jfs, uint64_t prev_jfs)
47 | burst = min(burst, BURST_MAX);
48 | sent = 0;
49 |
50 | + uint64_t head_ts = ((struct vidqent *)vtx->sendq.head->data)->ts64;
51 | + uint64_t tail_ts = ((struct vidqent *)vtx->sendq.tail->data)->ts64;
52 | +
53 | + vtx->ts_latency = tail_ts - head_ts;
54 | +
55 | + if (vtx->ts_latency > (3600 * 5) && /* (3600 = 40ms) */
56 | + (jfs - jfs_latency) > 500) {
57 | + uint64_t rtpc =
58 | + video_calc_rtp_timestamp_fix(tmr_jiffies_usec());
59 | + uint64_t overall = rtpc - head_ts;
60 | +
61 | + warning("video_poll: %p: vtx latency: %f (overall %f)\n", vtx,
62 | + video_calc_seconds(vtx->ts_latency),
63 | + video_calc_seconds(overall));
64 | + jfs_latency = jfs;
65 | + }
66 | +
67 | while (le) {
68 | struct vidqent *qent = le->data;
69 | le = le->next;
70 | @@ -315,6 +336,8 @@ static void vidqueue_poll(struct vtx *vtx, uint64_t jfs, uint64_t prev_jfs)
71 | list_move(&qent->le, &vtx->sendqnb);
72 |
73 | if (sent > burst) {
74 | + warning("video_poll: %p: burst break %zu\n", vtx,
75 | + burst);
76 | break;
77 | }
78 | }
79 | @@ -412,7 +435,7 @@ static int packet_handler(bool marker, uint64_t ts,
80 | rtp_ts = vtx->ts_offset + (ts & 0xffffffff);
81 |
82 | err = vidqent_alloc(&qent, strm, marker, stream_pt_enc(strm), rtp_ts,
83 | - hdr, hdr_len, pld, pld_len);
84 | + ts, hdr, hdr_len, pld, pld_len);
85 | if (err)
86 | return err;
87 |
88 |
--------------------------------------------------------------------------------
/patches/baresip_video_remove_sendq_empty.patch:
--------------------------------------------------------------------------------
1 | diff --git a/src/video.c b/src/video.c
2 | index eec6a35c..f3371311 100644
3 | --- a/src/video.c
4 | +++ b/src/video.c
5 | @@ -449,7 +449,6 @@ static void encode_rtp_send(struct vtx *vtx, struct vidframe *frame,
6 | {
7 | struct le *le;
8 | int err = 0;
9 | - bool sendq_empty;
10 |
11 | if (!vtx->enc)
12 | return;
13 | @@ -471,15 +470,6 @@ static void encode_rtp_send(struct vtx *vtx, struct vidframe *frame,
14 | goto out;
15 | }
16 |
17 | - mtx_lock(&vtx->lock_tx);
18 | - sendq_empty = (vtx->sendq.head == NULL);
19 | - mtx_unlock(&vtx->lock_tx);
20 | -
21 | - if (!sendq_empty) {
22 | - ++vtx->skipc;
23 | - return;
24 | - }
25 | -
26 | mtx_lock(&vtx->lock_enc);
27 |
28 | /* Convert image */
29 |
--------------------------------------------------------------------------------
/patches/re_864.patch:
--------------------------------------------------------------------------------
1 | From fd508d48d05c82d0bf1717ffd826b7112a1aadd4 Mon Sep 17 00:00:00 2001
2 | From: Sebastian Reimers
3 | Date: Tue, 27 Jun 2023 08:15:44 +0200
4 | Subject: [PATCH] vidmix: allow different pixel format
5 |
6 | ---
7 | include/rem_vidmix.h | 1 +
8 | rem/vidmix/vidmix.c | 25 +++++++++++++++++++++----
9 | 2 files changed, 22 insertions(+), 4 deletions(-)
10 |
11 | diff --git a/include/rem_vidmix.h b/include/rem_vidmix.h
12 | index 021ae0b6f..b775bbd7a 100644
13 | --- a/include/rem_vidmix.h
14 | +++ b/include/rem_vidmix.h
15 | @@ -19,6 +19,7 @@ typedef void (vidmix_frame_h)(uint64_t ts, const struct vidframe *frame,
16 | void *arg);
17 |
18 | int vidmix_alloc(struct vidmix **mixp);
19 | +void vidmix_set_fmt(struct vidmix *mix, enum vidfmt fmt);
20 | int vidmix_source_alloc(struct vidmix_source **srcp, struct vidmix *mix,
21 | const struct vidsz *sz, unsigned fps, bool content,
22 | vidmix_frame_h *fh, void *arg);
23 | diff --git a/rem/vidmix/vidmix.c b/rem/vidmix/vidmix.c
24 | index 4efd02e9a..a40af93c0 100644
25 | --- a/rem/vidmix/vidmix.c
26 | +++ b/rem/vidmix/vidmix.c
27 | @@ -27,6 +27,7 @@ struct vidmix {
28 | struct list srcl;
29 | bool initialized;
30 | uint32_t next_pidx;
31 | + enum vidfmt fmt;
32 | };
33 |
34 | struct vidmix_source {
35 | @@ -347,6 +348,7 @@ int vidmix_alloc(struct vidmix **mixp)
36 | goto out;
37 | }
38 |
39 | + mix->fmt = VID_FMT_YUV420P;
40 | mix->initialized = true;
41 |
42 | out:
43 | @@ -359,6 +361,21 @@ int vidmix_alloc(struct vidmix **mixp)
44 | }
45 |
46 |
47 | +/**
48 | + * Set video mixer pixel format
49 | + *
50 | + * @param mix Video mixer
51 | + * @param fmt Pixel format
52 | + */
53 | +void vidmix_set_fmt(struct vidmix *mix, enum vidfmt fmt)
54 | +{
55 | + if (!mix)
56 | + return;
57 | +
58 | + mix->fmt = fmt;
59 | +}
60 | +
61 | +
62 | /**
63 | * Allocate a video mixer source
64 | *
65 | @@ -400,7 +417,7 @@ int vidmix_source_alloc(struct vidmix_source **srcp, struct vidmix *mix,
66 | }
67 |
68 | if (sz) {
69 | - err = vidframe_alloc(&src->frame_tx, VID_FMT_YUV420P, sz);
70 | + err = vidframe_alloc(&src->frame_tx, mix->fmt, sz);
71 | if (err)
72 | goto out;
73 |
74 | @@ -584,7 +601,7 @@ int vidmix_source_set_size(struct vidmix_source *src, const struct vidsz *sz)
75 | if (src->frame_tx && vidsz_cmp(&src->frame_tx->size, sz))
76 | return 0;
77 |
78 | - err = vidframe_alloc(&frame, VID_FMT_YUV420P, sz);
79 | + err = vidframe_alloc(&frame, src->mix->fmt, sz);
80 | if (err)
81 | return err;
82 |
83 | @@ -730,7 +747,7 @@ void vidmix_source_set_focus_idx(struct vidmix_source *src, uint32_t pidx)
84 | */
85 | void vidmix_source_put(struct vidmix_source *src, const struct vidframe *frame)
86 | {
87 | - if (!src || !frame || frame->fmt != VID_FMT_YUV420P)
88 | + if (!src || !frame || frame->fmt != src->mix->fmt)
89 | return;
90 |
91 | if (!src->frame_rx || !vidsz_cmp(&src->frame_rx->size, &frame->size)) {
92 | @@ -738,7 +755,7 @@ void vidmix_source_put(struct vidmix_source *src, const struct vidframe *frame)
93 | struct vidframe *frm;
94 | int err;
95 |
96 | - err = vidframe_alloc(&frm, VID_FMT_YUV420P, &frame->size);
97 | + err = vidframe_alloc(&frm, src->mix->fmt, &frame->size);
98 | if (err)
99 | return;
100 |
101 |
--------------------------------------------------------------------------------
/patches/re_877.patch:
--------------------------------------------------------------------------------
1 | From e25ce258f55d2a540061c37bc5af4b3b921203ac Mon Sep 17 00:00:00 2001
2 | From: Sebastian Reimers
3 | Date: Wed, 12 Jul 2023 17:10:00 +0200
4 | Subject: [PATCH 1/2] aumix: add record sum handler
5 |
6 | ---
7 | include/rem_aumix.h | 1 +
8 | rem/aumix/aumix.c | 57 ++++++++++++++++++++++++++++++++++++++++++++-
9 | 2 files changed, 57 insertions(+), 1 deletion(-)
10 |
11 | diff --git a/include/rem_aumix.h b/include/rem_aumix.h
12 | index 0f17db0a3..41f715df6 100644
13 | --- a/include/rem_aumix.h
14 | +++ b/include/rem_aumix.h
15 | @@ -21,6 +21,7 @@ typedef void (aumix_read_h)(struct auframe *af, void *arg);
16 | int aumix_alloc(struct aumix **mixp, uint32_t srate,
17 | uint8_t ch, uint32_t ptime);
18 | void aumix_recordh(struct aumix *mix, aumix_record_h *recordh);
19 | +void aumix_record_sumh(struct aumix *mix, aumix_record_h *recordh);
20 | int aumix_playfile(struct aumix *mix, const char *filepath);
21 | uint32_t aumix_source_count(const struct aumix *mix);
22 | int aumix_source_alloc(struct aumix_source **srcp, struct aumix *mix,
23 | diff --git a/rem/aumix/aumix.c b/rem/aumix/aumix.c
24 | index 4ac348bb1..1ce3ef3da 100644
25 | --- a/rem/aumix/aumix.c
26 | +++ b/rem/aumix/aumix.c
27 | @@ -31,6 +31,8 @@ struct aumix {
28 | uint32_t srate;
29 | uint8_t ch;
30 | aumix_record_h *recordh;
31 | + aumix_record_h *record_sumh;
32 | + struct auframe rec_sum;
33 | bool run;
34 | };
35 |
36 | @@ -202,6 +204,38 @@ static int aumix_thread(void *arg)
37 | src->fh(mix_frame, mix->frame_size, src->arg);
38 | }
39 |
40 | + if (mix->record_sumh) {
41 | + struct le *cle;
42 | +
43 | + memcpy(mix_frame, base_frame, mix->frame_size * 2);
44 | +
45 | + LIST_FOREACH(&mix->srcl, cle)
46 | + {
47 | + struct aumix_source *csrc = cle->data;
48 | + int32_t sample;
49 | +
50 | + if (csrc->muted)
51 | + continue;
52 | +
53 | + for (size_t i = 0; i < mix->frame_size; i++) {
54 | + sample = mix_frame[i] + csrc->frame[i];
55 | +
56 | + /* soft clipping */
57 | + if (sample >= 32767)
58 | + sample = 32767;
59 | + if (sample <= -32767)
60 | + sample = -32767;
61 | +
62 | + mix_frame[i] = (int16_t)sample;
63 | + }
64 | + }
65 | +
66 | + mix->rec_sum.timestamp = now;
67 | + mix->rec_sum.sampv = mix_frame;
68 | +
69 | + mix->record_sumh(&mix->rec_sum);
70 | + }
71 | +
72 | ts += mix->ptime;
73 | }
74 |
75 | @@ -245,6 +279,10 @@ int aumix_alloc(struct aumix **mixp, uint32_t srate,
76 | mix->ch = ch;
77 | mix->recordh = NULL;
78 |
79 | + mix->rec_sum.ch = ch;
80 | + mix->rec_sum.srate = srate;
81 | + mix->rec_sum.sampc = mix->frame_size;
82 | +
83 | err = mtx_init(&mix->mutex, mtx_plain) != thrd_success;
84 | if (err) {
85 | err = ENOMEM;
86 | @@ -276,7 +314,7 @@ int aumix_alloc(struct aumix **mixp, uint32_t srate,
87 |
88 |
89 | /**
90 | - * Add record handler
91 | + * Add mulitrack record handler (each source can be identified by auframe->id)
92 | *
93 | * @param mix Audio mixer
94 | * @param recordh Record Handler
95 | @@ -292,6 +330,23 @@ void aumix_recordh(struct aumix *mix, aumix_record_h *recordh)
96 | }
97 |
98 |
99 | +/**
100 | + * Add single track record handler
101 | + *
102 | + * @param mix Audio mixer
103 | + * @param recordh Record Handler
104 | + */
105 | +void aumix_record_sumh(struct aumix *mix, aumix_record_h *recordh)
106 | +{
107 | + if (!mix)
108 | + return;
109 | +
110 | + mtx_lock(&mix->mutex);
111 | + mix->record_sumh = recordh;
112 | + mtx_unlock(&mix->mutex);
113 | +}
114 | +
115 | +
116 | /**
117 | * Load audio file for mixer announcements
118 | *
119 |
120 | From 42d40c8793234360d9c0ac6f56954e1ce0b46e03 Mon Sep 17 00:00:00 2001
121 | From: Sebastian Reimers
122 | Date: Wed, 12 Jul 2023 17:14:00 +0200
123 | Subject: [PATCH 2/2] fix wording
124 |
125 | ---
126 | rem/aumix/aumix.c | 2 +-
127 | 1 file changed, 1 insertion(+), 1 deletion(-)
128 |
129 | diff --git a/rem/aumix/aumix.c b/rem/aumix/aumix.c
130 | index 1ce3ef3da..acc0e2ab0 100644
131 | --- a/rem/aumix/aumix.c
132 | +++ b/rem/aumix/aumix.c
133 | @@ -314,7 +314,7 @@ int aumix_alloc(struct aumix **mixp, uint32_t srate,
134 |
135 |
136 | /**
137 | - * Add mulitrack record handler (each source can be identified by auframe->id)
138 | + * Add multitrack record handler (each source can be identified by auframe->id)
139 | *
140 | * @param mix Audio mixer
141 | * @param recordh Record Handler
142 |
--------------------------------------------------------------------------------
/patches/re_aubuf_timestamp_order_fix.patch:
--------------------------------------------------------------------------------
1 | diff --git a/rem/aubuf/aubuf.c b/rem/aubuf/aubuf.c
2 | index 71409bc..142d3a4 100644
3 | --- a/rem/aubuf/aubuf.c
4 | +++ b/rem/aubuf/aubuf.c
5 | @@ -266,7 +266,14 @@ int aubuf_append_auframe(struct aubuf *ab, struct mbuf *mb,
6 | auframe_bytes_to_timestamp(&f->af, ab->wr_sz);
7 | }
8 |
9 | - list_insert_sorted(&ab->afl, frame_less_equal, NULL, &f->le, f);
10 | + /* Workaround: looks like read timestamp calculation is not accurate
11 | + * enough if 960 samples are written and 1024 are read regulary */
12 | + if (ab->live)
13 | + list_insert_sorted(&ab->afl, frame_less_equal, NULL, &f->le,
14 | + f);
15 | + else
16 | + list_append(&ab->afl, &f->le, f);
17 | +
18 | ab->cur_sz += sz;
19 | ab->wr_sz += sz;
20 |
21 |
--------------------------------------------------------------------------------
/patches/re_vidmix_clear.patch:
--------------------------------------------------------------------------------
1 | diff --git a/rem/vidmix/vidmix.c b/rem/vidmix/vidmix.c
2 | index c603ccf..8813a4b 100644
3 | --- a/rem/vidmix/vidmix.c
4 | +++ b/rem/vidmix/vidmix.c
5 | @@ -217,10 +217,8 @@ static int vidmix_thread(void *arg)
6 |
7 | mtx_lock(&mix->rwlock);
8 |
9 | - if (src->clear) {
10 | - clear_frame(src->frame_tx);
11 | - src->clear = false;
12 | - }
13 | + /* always clear first */
14 | + clear_frame(src->frame_tx);
15 |
16 | for (le=mix->srcl.head, n=0; le; le=le->next) {
17 |
18 |
--------------------------------------------------------------------------------
/src/avatar.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 |
5 | #include
6 |
7 | #define BASE64_PNG "\"data:image/png;base64,"
8 | #define BASE64_PNG_SZ sizeof(BASE64_PNG) - 1
9 |
10 | enum file_type { FILE_NULL, FILE_JPEG, FILE_PNG };
11 |
12 | struct avatar {
13 | struct http_conn *conn;
14 | struct mbuf *mb;
15 | struct session *sess;
16 | void *arg;
17 | re_async_h *cb;
18 | enum file_type type;
19 | };
20 |
21 |
22 | static int work(void *arg)
23 | {
24 | struct avatar *avatar = arg;
25 | struct mbuf *mb = avatar->mb;
26 | FILE *img;
27 | int w = 256;
28 | int h = 256;
29 | gdImagePtr in = NULL, out;
30 | struct pl pl = PL_INIT;
31 | int err = 0;
32 | char file[PATH_SZ];
33 |
34 | if (mbuf_get_left(mb) < BASE64_PNG_SZ + 1) /* offset + ending '"' */
35 | return EINVAL;
36 |
37 | bool base64 = str_ncmp((const char *)mbuf_buf(mb), BASE64_PNG,
38 | BASE64_PNG_SZ) == 0;
39 |
40 | if (base64) {
41 | warning("avatar: base64\n");
42 | pl.l = mbuf_get_left(mb) * 2;
43 | pl.p = mem_zalloc(pl.l, NULL);
44 | if (!pl.p)
45 | return ENOMEM;
46 |
47 | mbuf_advance(mb, BASE64_PNG_SZ);
48 |
49 | err = base64_decode((char *)mbuf_buf(mb),
50 | mbuf_get_left(mb) - 1, (uint8_t *)pl.p,
51 | &pl.l);
52 | if (err) {
53 | mem_deref((void *)pl.p);
54 | warning("avatar: base64_decode failed %m\n", err);
55 | goto err;
56 | }
57 |
58 | in = gdImageCreateFromPngPtr((int)pl.l, (void *)pl.p);
59 | if (!in) {
60 | warning("avatar: create image failed\n");
61 | err = EIO;
62 | goto err;
63 | }
64 | }
65 | else {
66 | if (avatar->type == FILE_PNG) {
67 | in = gdImageCreateFromPngPtr((int)mbuf_get_left(mb),
68 | mbuf_buf(mb));
69 | }
70 | else if (avatar->type == FILE_JPEG) {
71 | in = gdImageCreateFromJpegPtr((int)mbuf_get_left(mb),
72 | mbuf_buf(mb));
73 | }
74 | else {
75 | warning("avatar: unkown file extension\n");
76 | err = EIO;
77 | goto err;
78 | }
79 |
80 | if (!in) {
81 | warning("avatar: create image failed\n");
82 | err = EIO;
83 | goto err;
84 | }
85 | }
86 |
87 | gdImageSetInterpolationMethod(in, GD_BILINEAR_FIXED);
88 | out = gdImageScale(in, w, h);
89 | if (!out) {
90 | gdImageDestroy(in);
91 | warning("avatar: image scale failed\n");
92 | err = EIO;
93 | goto err;
94 | }
95 |
96 | debug("write %s/webui/public/avatars/%s.[png,webp]\n", slmix()->path,
97 | avatar->sess->user->id);
98 |
99 | /* PNG */
100 | re_snprintf(file, sizeof(file), "%s/webui/public/avatars/%s.png",
101 | slmix()->path, avatar->sess->user->id);
102 |
103 | err = fs_fopen(&img, file, "w+");
104 | if (err) {
105 | warning("avatar: write png failed %m\n", err);
106 | goto out;
107 | }
108 |
109 | out->saveAlphaFlag = true;
110 | gdImagePng(out, img);
111 | fclose(img);
112 | chmod(file, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
113 |
114 | /* WEBP */
115 | re_snprintf(file, sizeof(file), "%s/webui/public/avatars/%s.webp",
116 | slmix()->path, avatar->sess->user->id);
117 | err = fs_fopen(&img, file, "w+");
118 | if (err) {
119 | warning("avatar: write webp failed %m\n", err);
120 | goto out;
121 | }
122 |
123 | out->saveAlphaFlag = true;
124 | gdImageWebp(out, img);
125 | fclose(img);
126 | chmod(file, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
127 |
128 | out:
129 | gdImageDestroy(in);
130 | gdImageDestroy(out);
131 |
132 | err:
133 | mem_deref((void *)pl.p);
134 |
135 | return err;
136 | }
137 |
138 |
139 | static void callback(int err, void *arg)
140 | {
141 | struct avatar *avatar = arg;
142 | char *json = NULL;
143 |
144 | /* use callback if provided and return */
145 | if (avatar->cb) {
146 | avatar->cb(err, avatar->arg);
147 | goto out;
148 | }
149 |
150 | /* generic http reply callback handling */
151 | if (err) {
152 | http_ereply(avatar->conn, 500, "Error");
153 | goto out;
154 | }
155 |
156 | err = user_event_json(&json, USER_ADDED, avatar->sess);
157 | if (err) {
158 | http_ereply(avatar->conn, 500, "Error");
159 | goto out;
160 | }
161 |
162 | http_sreply(avatar->conn, 201, "Created", "text/html", json,
163 | str_len(json), avatar->sess);
164 |
165 | out:
166 | mem_deref(json);
167 | mem_deref(avatar);
168 | }
169 |
170 |
171 | static void avatar_destruct(void *data)
172 | {
173 | struct avatar *avatar = data;
174 | mem_deref(avatar->conn);
175 | mem_deref(avatar->mb);
176 | mem_deref(avatar->sess);
177 | }
178 |
179 |
180 | int avatar_save(struct session *sess, struct http_conn *conn,
181 | const struct http_msg *msg, re_async_h *cb, void *arg)
182 | {
183 | struct avatar *avatar;
184 |
185 | if (!sess || !conn || !msg)
186 | return EINVAL;
187 |
188 | avatar = mem_zalloc(sizeof(struct avatar), avatar_destruct);
189 | if (!avatar)
190 | return ENOMEM;
191 |
192 | avatar->conn = mem_ref(conn);
193 | avatar->sess = mem_ref(sess);
194 | avatar->mb = mem_ref(msg->mb);
195 | avatar->arg = arg;
196 | avatar->cb = cb;
197 |
198 | if (pl_strcasecmp(&msg->ctyp.subtype, "jpeg") == 0) {
199 | avatar->type = FILE_JPEG;
200 | }
201 | else if (pl_strcasecmp(&msg->ctyp.subtype, "jpg") == 0) {
202 | avatar->type = FILE_JPEG;
203 | }
204 | else if (pl_strcasecmp(&msg->ctyp.subtype, "png") == 0) {
205 | avatar->type = FILE_PNG;
206 | }
207 |
208 | return re_thread_async(work, callback, avatar);
209 | }
210 |
211 |
212 | int avatar_delete(struct session *sess)
213 | {
214 | char file[PATH_SZ];
215 | int err;
216 |
217 | re_snprintf(file, sizeof(file), "%s/webui/public/avatars/%s.png",
218 | slmix()->path, sess->user->id);
219 | err = unlink(file);
220 | if (err)
221 | return errno;
222 |
223 | re_snprintf(file, sizeof(file), "%s/webui/public/avatars/%s.webp",
224 | slmix()->path, sess->user->id);
225 | err = unlink(file);
226 | if (err)
227 | return errno;
228 |
229 | return 0;
230 | }
231 |
--------------------------------------------------------------------------------
/src/chat.c:
--------------------------------------------------------------------------------
1 | #include
2 |
3 |
4 | static void destructor(void *data)
5 | {
6 | struct chat *chat = data;
7 |
8 | list_unlink(&chat->le);
9 | mem_deref(chat->user);
10 | }
11 |
12 |
13 | int chat_save(struct user *user, struct mix *mix, const struct http_msg *msg)
14 | {
15 | struct chat *chat;
16 | char *json;
17 | int err;
18 |
19 | if (!user || !mix || !msg || !str_isset(user->id))
20 | return EINVAL;
21 |
22 | if (mbuf_get_left(msg->mb) < 2) {
23 | return ENODATA;
24 | }
25 |
26 | if (mbuf_get_left(msg->mb) >= sizeof(chat->message)) {
27 | warning("chat_save: message to big\n");
28 | return EOVERFLOW;
29 | }
30 |
31 | chat = mem_zalloc(sizeof(struct chat), destructor);
32 | if (!chat)
33 | return ENOMEM;
34 |
35 | chat->user = mem_ref(user);
36 | chat->time = tmr_jiffies_rt_usec() / 1000 / 1000;
37 |
38 | msg->mb->pos += 1;
39 | msg->mb->end -= 1;
40 |
41 | mbuf_read_mem(msg->mb, (uint8_t *)chat->message,
42 | mbuf_get_left(msg->mb));
43 |
44 | list_append(&mix->chatl, &chat->le, chat);
45 |
46 | err = chat_event_json(&json, CHAT_ADDED, chat);
47 | if (err)
48 | return err;
49 |
50 | sl_ws_send_event_all(json);
51 |
52 | mem_deref(json);
53 |
54 | return 0;
55 | }
56 |
57 |
58 | int chat_json(char **json, struct mix *mix)
59 | {
60 | struct le *le;
61 | struct odict *o;
62 | struct odict *o_chat;
63 | struct odict *o_chats;
64 | char time_str[ITOA_BUFSZ];
65 | int err;
66 |
67 | if (!json || !mix)
68 | return EINVAL;
69 |
70 | err = odict_alloc(&o, 32);
71 | if (err)
72 | return err;
73 |
74 | err = odict_alloc(&o_chats, 32);
75 | if (err)
76 | return err;
77 |
78 | LIST_FOREACH(&mix->chatl, le)
79 | {
80 | struct chat *chat = le->data;
81 |
82 | err = odict_alloc(&o_chat, 8);
83 | if (err)
84 | goto out;
85 |
86 | odict_entry_add(o_chat, "user_id", ODICT_STRING,
87 | chat->user->id);
88 | odict_entry_add(o_chat, "user_name", ODICT_STRING,
89 | chat->user->name);
90 | odict_entry_add(
91 | o_chat, "time", ODICT_STRING,
92 | str_itoa((uint32_t)(chat->time), time_str, 10));
93 | odict_entry_add(o_chat, "msg", ODICT_STRING, chat->message);
94 |
95 | odict_entry_add(o_chats, "", ODICT_OBJECT, o_chat);
96 | o_chat = mem_deref(o_chat);
97 | }
98 |
99 | odict_entry_add(o, "chats", ODICT_ARRAY, o_chats);
100 |
101 | err = re_sdprintf(json, "%H", json_encode_odict, o);
102 |
103 | out:
104 | mem_deref(o);
105 | mem_deref(o_chats);
106 | return err;
107 | }
108 |
109 |
110 | int chat_event_json(char **json, enum user_event event, struct chat *chat)
111 | {
112 |
113 | struct odict *o;
114 | int err;
115 | char time_str[ITOA_BUFSZ];
116 |
117 | if (!json || !chat)
118 | return EINVAL;
119 |
120 | err = odict_alloc(&o, 8);
121 | if (err)
122 | return err;
123 |
124 | odict_entry_add(o, "type", ODICT_STRING, "chat");
125 |
126 | if (event == CHAT_ADDED)
127 | odict_entry_add(o, "event", ODICT_STRING, "chat_added");
128 | else {
129 | err = EINVAL;
130 | goto out;
131 | }
132 |
133 | odict_entry_add(o, "user_id", ODICT_STRING, chat->user->id);
134 | odict_entry_add(o, "user_name", ODICT_STRING, chat->user->name);
135 | odict_entry_add(o, "time", ODICT_STRING,
136 | str_itoa((uint32_t)(chat->time), time_str, 10));
137 | odict_entry_add(o, "msg", ODICT_STRING, chat->message);
138 |
139 | err = re_sdprintf(json, "%H", json_encode_odict, o);
140 |
141 | out:
142 | mem_deref(o);
143 |
144 | return err;
145 | }
146 |
--------------------------------------------------------------------------------
/src/http_client.c:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | static struct http_cli *client = NULL;
4 |
5 | struct http_conf sl_http_conf = {.conn_timeout = 10 * 1000,
6 | .recv_timeout = 60 * 1000,
7 | .idle_timeout = 900 * 1000};
8 |
9 | enum { HTTP_MAX_REDIRECTS = 10 };
10 |
11 | static void destroy(void *arg)
12 | {
13 | struct sl_httpconn *p = arg;
14 | mem_deref(p->conn);
15 | }
16 |
17 |
18 | static void resph(int err, const struct http_msg *msg, void *arg)
19 | {
20 | struct sl_httpconn *p = arg;
21 | char *url = NULL;
22 |
23 | if (!err && msg && msg->scode >= 301 && msg->scode <= 308) {
24 | const struct http_hdr *location =
25 | http_msg_hdr(msg, HTTP_HDR_LOCATION);
26 |
27 | if (++p->redirects > HTTP_MAX_REDIRECTS) {
28 | err = E2BIG;
29 | goto err;
30 | }
31 |
32 | err = pl_strdup(&url, &location->val);
33 | if (err)
34 | goto err;
35 |
36 | sl_httpc_req(p, SL_HTTP_GET, url, NULL);
37 | if (err)
38 | goto err;
39 |
40 | goto out;
41 | }
42 |
43 | err:
44 | if (p->slresph)
45 | p->slresph(err, msg, p->arg);
46 |
47 | out:
48 | mem_deref(p);
49 | mem_deref(url);
50 | }
51 |
52 |
53 | int sl_httpc_alloc(struct sl_httpconn **slconn, http_resp_h *slresph,
54 | http_data_h *datah, void *arg)
55 | {
56 | int err;
57 | struct sl_httpconn *p;
58 |
59 | if (!slconn || !client)
60 | return EINVAL;
61 |
62 | p = mem_zalloc(sizeof(struct sl_httpconn), destroy);
63 | if (!p)
64 | return ENOMEM;
65 |
66 | p->arg = arg;
67 | p->slresph = slresph;
68 |
69 | err = http_reqconn_alloc(&p->conn, client, resph, datah, p);
70 | if (err)
71 | mem_deref(p);
72 | else
73 | *slconn = p;
74 |
75 | return err;
76 | }
77 |
78 |
79 | int sl_httpc_req(struct sl_httpconn *slconn, enum sl_httpc_met sl_met,
80 | const char *url, struct mbuf *body)
81 | {
82 | struct pl uri, met;
83 | int err;
84 |
85 | if (!slconn || !url || !client)
86 | return EINVAL;
87 |
88 | switch (sl_met) {
89 | case SL_HTTP_GET:
90 | pl_set_str(&met, "GET");
91 | break;
92 | case SL_HTTP_POST:
93 | pl_set_str(&met, "POST");
94 | break;
95 | case SL_HTTP_PUT:
96 | pl_set_str(&met, "PUT");
97 | break;
98 | case SL_HTTP_PATCH:
99 | pl_set_str(&met, "PATCH");
100 | break;
101 | case SL_HTTP_DELETE:
102 | pl_set_str(&met, "DELETE");
103 | break;
104 | default:
105 | return ENOTSUP;
106 | }
107 |
108 | err = http_reqconn_set_method(slconn->conn, &met);
109 | if (err)
110 | return err;
111 |
112 | if (body) {
113 | err = http_reqconn_set_body(slconn->conn, body);
114 | if (err)
115 | return err;
116 | }
117 |
118 | mem_ref(slconn);
119 | pl_set_str(&uri, url);
120 | return http_reqconn_send(slconn->conn, &uri);
121 | }
122 |
123 |
124 | int sl_httpc_init(void)
125 | {
126 | int err;
127 |
128 | err = http_client_alloc(&client, net_dnsc(baresip_network()));
129 | if (err)
130 | return err;
131 |
132 | err = http_client_add_ca(client, "/etc/ssl/certs/ca-certificates.crt");
133 | if (err)
134 | return err;
135 |
136 | err = http_client_set_config(client, &sl_http_conf);
137 |
138 | return err;
139 | }
140 |
141 |
142 | void sl_httpc_close(void)
143 | {
144 | client = mem_deref(client);
145 | }
146 |
--------------------------------------------------------------------------------
/src/main.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 |
5 | static const char *modv[] = {
6 | "ice",
7 | "dtls_srtp",
8 |
9 | /* audio */
10 | "amix",
11 | "opus",
12 | "auresamp",
13 |
14 | /* video */
15 | "vp8",
16 | "avcodec",
17 | "vmix",
18 | };
19 |
20 | static char *config_file = NULL;
21 | static char *config_listen = NULL;
22 |
23 |
24 | const char *slmix_config_listen(void)
25 | {
26 | if (!config_listen)
27 | return "127.0.0.1";
28 | return config_listen;
29 | }
30 |
31 |
32 | static void signal_handler(int sig)
33 | {
34 | (void)sig;
35 | re_cancel();
36 | }
37 |
38 |
39 | static void usage(void)
40 | {
41 | (void)re_fprintf(stderr, "Usage: slmix [options]\n"
42 | "options:\n"
43 | "\t-h Help\n"
44 | "\t-c --config Load config file\n"
45 | "\t-l Listen IP\n"
46 | "\t-v Verbose debug\n");
47 | }
48 |
49 |
50 | static int slmix_getopt(int argc, char *const argv[])
51 | {
52 | #ifdef HAVE_GETOPT
53 | int index = 0;
54 | struct option options[] = {{"config", required_argument, 0, 'c'},
55 | {"help", 0, 0, 'h'},
56 | {"verbose", 0, 0, 'v'},
57 | {"listen", required_argument, 0, 'l'},
58 | {0, 0, 0, 0}};
59 | (void)re_printf(
60 | " _____ __ ___ __ _ __\n"
61 | " / ___// /___ ______/ (_)___ / / (_)___ / /__\n"
62 | " \\__ \\/ __/ / / / __ / / __ \\ / / / / __ \\/ //_/\n"
63 | " ___/ / /_/ /_/ / /_/ / / /_/ / / /___/ / / / / ,<\n"
64 | "/____/\\__/\\__,_/\\__,_/_/\\____(_)_____/_/_/ /_/_/|_|"
65 | "\n");
66 |
67 | (void)re_printf("Mix v%s-%s"
68 | " Copyright (C) 2013 - 2025"
69 | " Sebastian Reimers\n\n",
70 | SLMIX_VERSION, slmix_git_revision());
71 |
72 | for (;;) {
73 | const int c =
74 | getopt_long(argc, argv, "c:hvl:", options, &index);
75 | if (c < 0)
76 | break;
77 |
78 | switch (c) {
79 |
80 | case 'c':
81 | if (!fs_isfile(optarg)) {
82 | warning("config not found: %s\n", optarg);
83 | return EINVAL;
84 | }
85 | str_dup(&config_file, optarg);
86 | break;
87 | case 'h':
88 | usage();
89 | return -2;
90 | case 'v':
91 | log_enable_debug(true);
92 | break;
93 | case 'l':
94 | str_dup(&config_listen, optarg);
95 | break;
96 | default:
97 | usage();
98 | return EINVAL;
99 | }
100 | }
101 | #else
102 | (void)argc;
103 | (void)argv;
104 | #endif
105 |
106 | return 0;
107 | }
108 |
109 |
110 | int main(int argc, char *const argv[])
111 | {
112 | (void)argc;
113 | (void)argv;
114 | int err;
115 | struct config *config;
116 | struct mix *mix = slmix();
117 |
118 | const char *conf =
119 | /* "sip_listen 0.0.0.0:5060\n" */
120 | "call_max_calls 10\n" /* SIP incoming only */
121 | "sip_verify_server yes\n"
122 | "audio_buffer 40-100\n"
123 | "audio_buffer_mode fixed\n"
124 | "audio_silence -35.0\n"
125 | "audio_jitter_buffer_type adaptive\n"
126 | "audio_jitter_buffer_ms 60-200\n"
127 | "video_jitter_buffer_type adaptive\n"
128 | "video_jitter_buffer_ms 100-200\n"
129 | "video_jitter_buffer_size 1000\n"
130 | "opus_bitrate 64000\n"
131 | "ice_policy relay\n"
132 | "video_size 1920x1080\n"
133 | "video_bitrate 2500000\n"
134 | "video_sendrate 10000000\n" /* max burst send */
135 | "video_burst_bit 1000000\n" /* max burst send */
136 | "video_fps 24\n"
137 | "avcodec_keyint 10\n"
138 | "avcodec_h265enc nil\n"
139 | "avcodec_h265dec nil\n"
140 | "vp8_enc_threads 4\n"
141 | "vp8_enc_cpuused -8\n"
142 | #if 0
143 | "videnc_format nv12\n"
144 | "avcodec_h264enc h264_nvenc\n"
145 | "avcodec_h264dec h264\n"
146 | "avcodec_hwaccel cuda\n"
147 | #endif
148 | "audio_txmode thread\n";
149 |
150 | /*
151 | * turn off buffering on stdout
152 | */
153 | setbuf(stdout, NULL);
154 |
155 | err = libre_init();
156 | if (err)
157 | return err;
158 |
159 | #ifdef RE_TRACE_ENABLED
160 | err = re_trace_init("re_trace.json");
161 | if (err)
162 | return err;
163 | #endif
164 |
165 | err = slmix_getopt(argc, argv);
166 | if (err)
167 | return err;
168 |
169 | fd_setsize(-1);
170 | re_thread_async_init(8);
171 |
172 | (void)sys_coredump_set(true);
173 |
174 | err = conf_configure_buf((const uint8_t *)conf, str_len(conf));
175 | if (err) {
176 | warning("conf_configure_buf failed: %m\n", err);
177 | return err;
178 | }
179 |
180 | config = conf_config();
181 |
182 | config->net.use_linklocal = false;
183 |
184 | config->audio.srate_play = 48000;
185 | config->audio.srate_src = 48000;
186 | config->audio.channels_play = 1;
187 | config->audio.channels_src = 1;
188 |
189 | config->avt.rtcp_mux = true;
190 | config->avt.rtp_stats = true;
191 |
192 | err = slmix_config(config_file);
193 | if (err)
194 | return err;
195 |
196 | err = baresip_init(config);
197 | if (err) {
198 | warning("baresip_init failed (%m)\n", err);
199 | return err;
200 | }
201 |
202 | err = ua_init("StudioLinkMix", true, true, true);
203 | if (err) {
204 | warning("ua_init failed (%m)\n", err);
205 | return err;
206 | }
207 |
208 | for (size_t i = 0; i < RE_ARRAY_SIZE(modv); i++) {
209 |
210 | err = module_load(".", modv[i]);
211 | if (err) {
212 | warning("could not pre-load module"
213 | " '%s' (%m)\n",
214 | modv[i], err);
215 | }
216 | }
217 |
218 | err = slmix_init();
219 | if (err)
220 | return err;
221 |
222 | sl_ws_init();
223 | err = slmix_http_listen(&mix->httpsock, mix);
224 | if (err)
225 | return err;
226 |
227 | re_main(signal_handler);
228 |
229 | sl_ws_close();
230 | slmix_close();
231 |
232 | ua_stop_all(true);
233 | ua_close();
234 |
235 | module_app_unload();
236 | conf_close();
237 |
238 | config_file = mem_deref(config_file);
239 | config_listen = mem_deref(config_listen);
240 |
241 | baresip_close();
242 | mod_close();
243 |
244 | re_thread_async_close();
245 | #ifdef RE_TRACE_ENABLED
246 | re_trace_close();
247 | #endif
248 | tmr_debug();
249 | libre_close();
250 | mem_debug();
251 |
252 | return 0;
253 | }
254 |
--------------------------------------------------------------------------------
/src/sip.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | static struct ua *sip_ua;
5 |
6 |
7 | static void ua_event_handler(enum ua_event ev, struct bevent *event, void *arg)
8 | {
9 | struct mix *mix = arg;
10 | struct call *call = bevent_get_call(event);
11 |
12 | switch (ev) {
13 |
14 | case UA_EVENT_CALL_INCOMING:
15 | if (call_state(call) != CALL_STATE_INCOMING)
16 | return;
17 |
18 | struct session *sess;
19 | const char *peer = call_peeruri(call);
20 | struct pl peer_pl = PL_INIT;
21 |
22 | pl_set_str(&peer_pl, peer);
23 |
24 | slmix_session_alloc(&sess, mix, NULL, NULL, &peer_pl, false,
25 | true);
26 |
27 | pl_strcpy(&peer_pl, sess->user->id, sizeof(sess->user->id));
28 |
29 | audio_set_devicename(call_audio(call), peer, peer);
30 | video_set_devicename(call_video(call), peer, peer);
31 |
32 | stream_enable(video_strm(call_video(call)), true);
33 | stream_enable(audio_strm(call_audio(call)), true);
34 |
35 | (void)call_answer(call, 200, VIDMODE_ON);
36 | info("auto answer call with %s\n", call_peeruri(call));
37 |
38 | amix_mute(peer, false, ++mix->next_speaker_id);
39 |
40 | sess->call = call;
41 | sess->connected = true;
42 | sess->user->video = true;
43 |
44 | slmix_disp_enable(mix, peer, true);
45 |
46 | slmix_source_append_all(mix, call, peer);
47 |
48 | break;
49 |
50 | case UA_EVENT_CALL_CLOSED: {
51 | slmix_source_deref(mix, call, NULL);
52 |
53 | break;
54 | }
55 | default:
56 | break;
57 | }
58 | }
59 |
60 |
61 | int slmix_sip_init(struct mix *mix)
62 | {
63 | int err;
64 | char aor[128];
65 |
66 | err = bevent_register(ua_event_handler, mix);
67 | if (err)
68 | return err;
69 |
70 | re_snprintf(aor, sizeof(aor), ";regint=0");
71 |
72 | err = ua_alloc(&sip_ua, aor);
73 | if (err)
74 | return err;
75 |
76 | err = ua_register(sip_ua);
77 |
78 | return err;
79 | }
80 |
81 |
82 | int slmix_sip_close(void)
83 | {
84 | bevent_unregister(ua_event_handler);
85 |
86 | return 0;
87 | }
88 |
--------------------------------------------------------------------------------
/src/users.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include "mix.h"
4 |
5 | enum { MAX_LISTENERS = 100 };
6 |
7 |
8 | int users_json(char **json, struct mix *mix)
9 | {
10 | struct le *le;
11 | struct odict *o;
12 | struct odict *o_user;
13 | struct odict *o_users;
14 | int err;
15 |
16 | if (!json || !mix)
17 | return EINVAL;
18 |
19 | err = odict_alloc(&o, 8);
20 | if (err)
21 | return err;
22 |
23 | err = odict_alloc(&o_users, 32);
24 | if (err)
25 | return err;
26 |
27 | int listeners = 0;
28 | LIST_FOREACH(&mix->sessl, le)
29 | {
30 | struct session *sess = le->data;
31 |
32 | if (!sess->connected)
33 | continue;
34 |
35 | if (!sess->user->speaker) {
36 | if (++listeners > MAX_LISTENERS)
37 | continue;
38 | }
39 |
40 | err = odict_alloc(&o_user, 8);
41 | if (err)
42 | goto out;
43 |
44 | odict_entry_add(o_user, "id", ODICT_STRING, sess->user->id);
45 | odict_entry_add(o_user, "speaker_id", ODICT_INT,
46 | sess->user->speaker_id);
47 | odict_entry_add(o_user, "pidx", ODICT_INT, sess->user->pidx);
48 | odict_entry_add(o_user, "name", ODICT_STRING,
49 | sess->user->name);
50 | odict_entry_add(o_user, "speaker", ODICT_BOOL,
51 | sess->user->speaker);
52 | odict_entry_add(o_user, "host", ODICT_BOOL, sess->user->host);
53 | odict_entry_add(o_user, "video", ODICT_BOOL,
54 | sess->user->video);
55 | odict_entry_add(o_user, "audio", ODICT_BOOL,
56 | sess->user->audio);
57 | odict_entry_add(o_user, "hand", ODICT_BOOL, sess->user->hand);
58 | odict_entry_add(o_user, "solo", ODICT_BOOL, sess->user->solo);
59 | odict_entry_add(o_user, "webrtc", ODICT_BOOL,
60 | sess->pc ? true : false);
61 |
62 | odict_entry_add(o_users, sess->user->id, ODICT_OBJECT, o_user);
63 | o_user = mem_deref(o_user);
64 | }
65 |
66 | odict_entry_add(o, "type", ODICT_STRING, "users");
67 | odict_entry_add(o, "listeners", ODICT_INT, listeners);
68 | odict_entry_add(o, "users", ODICT_ARRAY, o_users);
69 |
70 | err = re_sdprintf(json, "%H", json_encode_odict, o);
71 |
72 | out:
73 | mem_deref(o);
74 | mem_deref(o_users);
75 | return err;
76 | }
77 |
78 |
79 | int user_event_json(char **json, enum user_event event, struct session *sess)
80 | {
81 | struct odict *o;
82 | int err;
83 |
84 | if (!json || !sess)
85 | return EINVAL;
86 |
87 | err = odict_alloc(&o, 8);
88 | if (err)
89 | return err;
90 |
91 | odict_entry_add(o, "type", ODICT_STRING, "user");
92 |
93 | if (event == USER_ADDED)
94 | odict_entry_add(o, "event", ODICT_STRING, "added");
95 | else if (event == USER_UPDATED)
96 | odict_entry_add(o, "event", ODICT_STRING, "updated");
97 | else if (event == USER_DELETED)
98 | odict_entry_add(o, "event", ODICT_STRING, "deleted");
99 | else {
100 | err = EINVAL;
101 | goto out;
102 | }
103 |
104 | odict_entry_add(o, "id", ODICT_STRING, sess->user->id);
105 | odict_entry_add(o, "speaker_id", ODICT_INT, sess->user->speaker_id);
106 | odict_entry_add(o, "pidx", ODICT_INT, sess->user->pidx);
107 | odict_entry_add(o, "name", ODICT_STRING, sess->user->name);
108 | odict_entry_add(o, "speaker", ODICT_BOOL, sess->user->speaker);
109 | odict_entry_add(o, "host", ODICT_BOOL, sess->user->host);
110 | odict_entry_add(o, "video", ODICT_BOOL, sess->user->video);
111 | odict_entry_add(o, "audio", ODICT_BOOL, sess->user->audio);
112 | odict_entry_add(o, "hand", ODICT_BOOL, sess->user->hand);
113 | odict_entry_add(o, "solo", ODICT_BOOL, sess->user->solo);
114 | odict_entry_add(o, "webrtc", ODICT_BOOL, sess->pc ? true : false);
115 | odict_entry_add(o, "calling", ODICT_BOOL, sess->user->calling);
116 |
117 | err = re_sdprintf(json, "%H", json_encode_odict, o);
118 |
119 | out:
120 | mem_deref(o);
121 |
122 | return err;
123 | }
124 |
--------------------------------------------------------------------------------
/tests/phpunit/.gitignore:
--------------------------------------------------------------------------------
1 | vendor
2 | *.cache
3 |
--------------------------------------------------------------------------------
/tests/phpunit/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "require": {
3 | "phpunit/phpunit": "^11.5",
4 | "guzzlehttp/guzzle": "^7.9",
5 | "phrity/websocket": "^3.4"
6 | },
7 | "autoload-dev": {
8 | "psr-4": {
9 | "Tests\\": "tests/"
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/tests/phpunit/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 | tests
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/tests/phpunit/tests/ChatTest.php:
--------------------------------------------------------------------------------
1 | login("alice", ClientAuth::Host);
15 |
16 | $bob = new Client();
17 | $bob->login("bob", ClientAuth::Audience);
18 |
19 | $msg = $alice->ws_next(); /* connect websocket */
20 | $this->assertEquals("users", $msg->type);
21 |
22 | $bob->post("/api/v1/chat", "\"Hello World\"");
23 | $msg = $alice->ws_next("chat", "chat_added");
24 | $this->assertEquals("Hello World", $msg->msg);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tests/phpunit/tests/Client.php:
--------------------------------------------------------------------------------
1 | cookies = new \GuzzleHttp\Cookie\CookieJar;
25 | $this->client = new HttpClient(['base_uri' => 'http://127.0.0.1:9999', 'http_errors' => false]);
26 | $this->ws = new \WebSocket\Client("ws://127.0.0.1:9999/ws/v1/users");
27 | }
28 |
29 | function get($url)
30 | {
31 | return $this->client->request(
32 | 'GET',
33 | $url,
34 | ['cookies' => $this->cookies]
35 | );
36 | }
37 |
38 | function delete($url, $body = NULL)
39 | {
40 | return $this->client->request(
41 | 'DELETE',
42 | $url,
43 | ['cookies' => $this->cookies, 'body' => $body]
44 | );
45 | }
46 |
47 | function post($url, $body = NULL)
48 | {
49 | return $this->client->request(
50 | 'POST',
51 | $url,
52 | ['cookies' => $this->cookies, 'body' => $body]
53 | );
54 | }
55 |
56 | function login($name = NULL, ClientAuth $auth = ClientAuth::Audience)
57 | {
58 | $this->post('/api/v1/client/connect', $auth->value);
59 | $sess_id = $this->cookies->getCookieByName("mix_session")->getValue();
60 | $this->ws->addHeader("Cookie", "mix_session=" . $sess_id);
61 |
62 | if ($name)
63 | $this->post('/api/v1/client/name', $name);
64 | }
65 |
66 | function logout()
67 | {
68 | return $this->delete('/api/v1/client');
69 | }
70 |
71 | function ws_next(string | NULL $type = NULL, string | NULL $event = NULL)
72 | {
73 | if (!$type) {
74 | $json = json_decode($this->ws->receive()->getContent());
75 | return $json;
76 | }
77 |
78 | $cnt = 10;
79 | while ($cnt--) {
80 | $json = json_decode($this->ws->receive()->getContent());
81 | if ($type === $json->type) {
82 | if (!$event)
83 | return $json;
84 | else if ($json->event === $event)
85 | return $json;
86 | }
87 | }
88 |
89 | throw new \Exception('ws_next event timedout');
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/tests/phpunit/tests/LoginTest.php:
--------------------------------------------------------------------------------
1 | login();
17 | $bob->login();
18 |
19 | $alice->login("alice");
20 | $bob->login("bob");
21 |
22 | $msg_bob = json_decode($bob->ws->receive()->getContent());
23 | $this->assertEquals("bob", $msg_bob->users[0]->name);
24 | $this->assertFalse($msg_bob->users[0]->speaker);
25 |
26 | $msg_alice = json_decode($alice->ws->receive()->getContent());
27 | $this->assertEquals("alice", $msg_alice->users[0]->name);
28 | $this->assertFalse($msg_alice->users[0]->speaker);
29 | $this->assertEquals("bob", $msg_alice->users[1]->name);
30 | }
31 |
32 |
33 | public function test_Alice_logins_as_host()
34 | {
35 | $alice = new Client();
36 | $alice->login("alice", ClientAuth::Host);
37 |
38 | $msg = json_decode($alice->ws->receive()->getContent());
39 |
40 | $this->assertTrue($msg->users[0]->host);
41 | }
42 |
43 |
44 | public function test_Alice_re_auth_as_host()
45 | {
46 | $alice = new Client();
47 | $alice->login("alice", ClientAuth::Guest);
48 | $alice->login("alice", ClientAuth::Host);
49 |
50 | $msg = json_decode($alice->ws->receive()->getContent());
51 |
52 | $this->assertTrue($msg->users[0]->host);
53 | }
54 |
55 |
56 | public function test_Alice_logout()
57 | {
58 | $alice = new Client();
59 | $alice->login("alice");
60 |
61 | $bob = new Client();
62 | $bob->login("bob");
63 |
64 | $msg_bob = $bob->ws_next();
65 | $this->assertEquals("users", $msg_bob->type);
66 |
67 | $alice->ws->close();
68 | $r = $alice->logout();
69 | $this->assertEquals(204, $r->getStatusCode());
70 |
71 | $msg_bob = $bob->ws_next("user", "deleted");
72 |
73 | $this->assertEquals("user", $msg_bob->type);
74 | $this->assertEquals("deleted", $msg_bob->event);
75 |
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/tests/phpunit/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | /tmp/slmix.log 2>&1 & }");
19 | $start_count = 0;
20 |
21 | while ($start_count++ < 1000) {
22 | usleep(5000);
23 | try {
24 | $client->get("/api/v1/sessions/connected");
25 | break;
26 | } catch (\Exception $_) {
27 | continue;
28 | }
29 | }
30 | }
31 |
32 | public static function tearDownAfterClass(): void
33 | {
34 | system("pkill slmix");
35 | }
36 |
37 | protected function setUp(): void {}
38 |
39 | protected function tearDown(): void
40 | {
41 | if (!$this->status()->isSuccess()) {
42 | system("cat /tmp/slmix.log");
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/webui/.env.development:
--------------------------------------------------------------------------------
1 | VITE_CSP_POLICY="default-src 'self'; connect-src *; script-src 'self'; form-action 'self'; img-src * data: blob:; style-src 'self' 'unsafe-inline';"
2 |
--------------------------------------------------------------------------------
/webui/.env.production:
--------------------------------------------------------------------------------
1 | VITE_CSP_POLICY="default-src 'self'; script-src 'self'; form-action 'self'; img-src * data: blob:; style-src 'self' 'unsafe-inline';"
2 |
--------------------------------------------------------------------------------
/webui/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | require('@rushstack/eslint-patch/modern-module-resolution')
3 |
4 | module.exports = {
5 | root: true,
6 | env: {
7 | browser: true,
8 | node: true,
9 | },
10 | extends: [
11 | 'eslint:recommended',
12 | 'plugin:vue/vue3-recommended',
13 | 'plugin:vue/vue3-essential',
14 | 'eslint:recommended',
15 | '@vue/eslint-config-typescript',
16 | '@vue/eslint-config-prettier',
17 | ],
18 | parserOptions: {
19 | ecmaVersion: 'latest',
20 | },
21 | }
22 |
--------------------------------------------------------------------------------
/webui/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/webui/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true,
4 | "tabWidth": 2,
5 | "printWidth": 120
6 | }
7 |
--------------------------------------------------------------------------------
/webui/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["Vue.volar"]
3 | }
4 |
--------------------------------------------------------------------------------
/webui/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Studio Link - Mix
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/webui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webui",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite --host",
8 | "build": "vue-tsc && vite build",
9 | "build-only": "vite build",
10 | "preview": "vite preview",
11 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --ignore-path .gitignore"
12 | },
13 | "dependencies": {
14 | "@headlessui/vue": "^1.7.4",
15 | "@heroicons/vue": "^2.0.13",
16 | "@tailwindcss/forms": "^0.5.3",
17 | "@tailwindcss/vite": "^4.0.8",
18 | "@vue/typescript-plugin": "^2.1.10",
19 | "@vueuse/components": "^13.0.0",
20 | "@vueuse/core": "^13.0.0",
21 | "cropperjs": "1.6.2",
22 | "markdown-it": "^14.0.0",
23 | "vue": "^3.2.41",
24 | "vue-router": "^4.1.6",
25 | "vue3-avataaars": "^1.0.12",
26 | "webrtc-adapter": "^9.0.1"
27 | },
28 | "devDependencies": {
29 | "@rushstack/eslint-patch": "^1.1.4",
30 | "@tailwindcss/typography": "^0.5.10",
31 | "@types/markdown-it": "^14.1.1",
32 | "@types/node": "^22.7.4",
33 | "@vitejs/plugin-vue": "^5.0.0",
34 | "@vue/eslint-config-prettier": "^10.1.0",
35 | "@vue/eslint-config-typescript": "^14.1.4",
36 | "eslint-plugin-vue": "^9.7.0",
37 | "tailwindcss": "^4.0.8",
38 | "typescript": "^5.6.2",
39 | "vite": "^6.0.1",
40 | "vue-tsc": "^2.0.29"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/webui/public/avatars/default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Studio-Link/mix/11ec61647941eadaf826e3385c2acd300928c26d/webui/public/avatars/default.png
--------------------------------------------------------------------------------
/webui/public/avatars/index.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Studio-Link/mix/11ec61647941eadaf826e3385c2acd300928c26d/webui/public/avatars/index.html
--------------------------------------------------------------------------------
/webui/public/download/index.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Studio-Link/mix/11ec61647941eadaf826e3385c2acd300928c26d/webui/public/download/index.html
--------------------------------------------------------------------------------
/webui/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Studio-Link/mix/11ec61647941eadaf826e3385c2acd300928c26d/webui/public/favicon.ico
--------------------------------------------------------------------------------
/webui/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /
3 |
--------------------------------------------------------------------------------
/webui/public/sendegate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Studio-Link/mix/11ec61647941eadaf826e3385c2acd300928c26d/webui/public/sendegate.png
--------------------------------------------------------------------------------
/webui/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
--------------------------------------------------------------------------------
/webui/src/assets/vue.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/webui/src/avdummy.ts:
--------------------------------------------------------------------------------
1 | import api from './api'
2 | import { State } from './ws/state'
3 |
4 | const width = 1280
5 | const height = 720
6 | const canvas = Object.assign(document.createElement('canvas'), { width, height })
7 | const ctx = canvas.getContext('2d')
8 | const image = new Image()
9 | const DRAW_MAX = 150
10 |
11 | let drawLoopIsRunning = false
12 | let drawCount = 0
13 |
14 | function draw() {
15 | if (!ctx)
16 | return
17 |
18 | ctx.fillStyle = "black";
19 | ctx.fillRect(0, 0, width, height)
20 | ctx.font = "48px serif";
21 | ctx.textAlign = "center"
22 | ctx.fillStyle = "gray";
23 | ctx.fillText(State.user.value.name, width / 2, height / 2 + image.height / 2);
24 | ctx.drawImage(image, width / 2 - (image.width / 2), (height / 2 - image.height / 2) - 48)
25 | }
26 |
27 | function drawLoop() {
28 | let wait = 500
29 | if (drawCount++ >= DRAW_MAX) {
30 | drawLoopIsRunning = false
31 | return
32 | }
33 |
34 | drawLoopIsRunning = true
35 |
36 | draw()
37 |
38 | setTimeout(() => {
39 | drawLoop()
40 | }, wait)
41 | }
42 |
43 | const black = () => {
44 | const stream = canvas.captureStream()
45 | if (!ctx)
46 | return stream.getVideoTracks()[0]
47 |
48 | ctx.fillRect(0, 0, width, height)
49 |
50 | image.src = '/avatars/' + api.user_id() + '.png'
51 | image.onload = () => {
52 | drawCount = 0
53 | //Chrome workaround: needs canvas frame change to start webrtc rtp
54 | drawLoop()
55 | }
56 | return stream.getVideoTracks()[0]
57 | }
58 |
59 | const silence = () => {
60 | const actx = new AudioContext()
61 | const oscillator = actx.createOscillator()
62 | const dst = actx.createMediaStreamDestination()
63 | oscillator.connect(dst)
64 | oscillator.start()
65 | return Object.assign(dst.stream.getAudioTracks()[0], { enabled: false })
66 | }
67 |
68 | export const Avdummy = {
69 | stream: null,
70 |
71 | async init() {
72 | this.stream = new MediaStream([black(), silence()])
73 | },
74 |
75 | getVideoTrack(): MediaStreamTrack | null {
76 | return this.stream?.getVideoTracks()[0] ?? null
77 | },
78 |
79 | getAudioTrack(): MediaStreamTrack | null {
80 | return this.stream?.getAudioTracks()[0] ?? null
81 | },
82 |
83 | async refresh() {
84 | draw()
85 | if (!drawLoopIsRunning) {
86 | drawCount = DRAW_MAX - 10
87 | drawLoop()
88 | }
89 | },
90 |
91 | async stopDrawLoop() {
92 | drawCount = DRAW_MAX
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/webui/src/components/Calls.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
10 |
11 |
13 | Close
14 |
15 |
16 |
17 |
18 |
22 |
23 | Your call is
24 | waiting...
25 |
26 |
27 |
28 |
29 | Cancel
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
46 |
47 |
49 | Close
50 |
51 |
52 |
53 |
54 |
58 |
59 | Call from {{
60 | user.name }}
61 |
62 |
63 |
64 |
65 | Accept
68 | Cancel
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
85 |
--------------------------------------------------------------------------------
/webui/src/components/ErrorText.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Error: {{ Error.text.value }}
12 |
13 |
14 |
15 |
18 | Dismiss
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | Audio playback permission is missing!
36 |
37 |
38 |
39 | Allow audio playback
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
65 |
--------------------------------------------------------------------------------
/webui/src/components/FooterLinks.vue:
--------------------------------------------------------------------------------
1 |
2 | {{version}}
3 |
4 |
5 |
9 |
--------------------------------------------------------------------------------
/webui/src/components/Listeners.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Audience
6 |
7 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
21 |
26 |
27 |
28 |
33 |
34 |
35 |
36 |
43 |
49 |
50 |
51 |
52 |
53 |
59 | On Stage
60 |
61 |
62 |
63 |
{{ item.name }}
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
79 |
--------------------------------------------------------------------------------
/webui/src/components/ReactionEmoji.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
21 |
22 |
23 |
24 |
31 | {{ reactions[emoji.reaction_id].emoji }}
32 |
33 |
34 |
35 |
36 |
50 |
51 |
74 |
--------------------------------------------------------------------------------
/webui/src/components/RecButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
18 | REC {{ State.record_timer.value }}
19 | REC Audio {{ State.record_timer.value }}
20 | Record
21 |
22 |
23 |
26 | Open options
27 |
28 |
29 |
37 |
40 |
41 |
42 | Record Audio only
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
61 |
--------------------------------------------------------------------------------
/webui/src/components/StudioNav.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 | Open/Close nav menu
10 |
11 |
12 |
13 |
14 |
15 |
52 |
53 |
54 |
62 |
--------------------------------------------------------------------------------
/webui/src/components/WebcamPhoto.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 | Snapshot
13 |
14 |
15 | Cam
16 |
22 |
23 |
24 | {{ item.label }}
25 |
26 |
27 |
28 |
29 |
34 | Save Avatar
35 |
36 |
37 |
38 |
61 |
--------------------------------------------------------------------------------
/webui/src/config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | base(): string {
3 | const pathSegments = location.pathname.split('/');
4 | const firstFolder = pathSegments[1]
5 | if (firstFolder === 'rooms')
6 | return '/rooms/' + pathSegments[2] + '/'
7 |
8 | return '/'
9 | },
10 | host(): string {
11 | return location.origin
12 | },
13 | ws_host(): string {
14 | if (location.protocol == 'https:')
15 | return 'wss://' + location.host
16 | else
17 | return 'ws://' + location.host
18 | },
19 | }
20 |
--------------------------------------------------------------------------------
/webui/src/error.ts:
--------------------------------------------------------------------------------
1 | import { ref } from 'vue'
2 | import router from './router'
3 |
4 | export const Error = {
5 | text: ref(''),
6 | video: ref(''),
7 | audio: ref(false),
8 |
9 | fatal(msg: string) {
10 | this.text.value = msg
11 | router.push({ name: 'FatalError' })
12 | },
13 |
14 | error(msg: string) {
15 | this.text.value = msg
16 | },
17 |
18 | errorVideo(msg: string) {
19 | this.video.value = msg
20 | },
21 |
22 | errorAudio(enable: boolean) {
23 | this.audio.value = enable
24 | },
25 |
26 | reset() {
27 | this.text.value = ''
28 | this.video.value = ''
29 | },
30 | }
31 |
--------------------------------------------------------------------------------
/webui/src/fadeout.ts:
--------------------------------------------------------------------------------
1 | let timeoutId: NodeJS.Timeout;
2 |
3 | function startTimer(): void {
4 | timeoutId = setTimeout(fadeOut, 4000);
5 | fadeIn()
6 | }
7 |
8 | function resetTimer(): void {
9 | // Reset the timer when there is any user activity
10 | clearTimeout(timeoutId);
11 | startTimer();
12 | }
13 |
14 | function resetTimerChat(): void {
15 | const elements = document.querySelectorAll("#chat_button") as NodeListOf;
16 | elements.forEach(element => {
17 | element.style.opacity = "1";
18 | });
19 | }
20 |
21 | function fadeIn(): void {
22 | const elements = document.querySelectorAll(".fadeout") as NodeListOf;
23 | elements.forEach(element => {
24 | element.style.opacity = "1";
25 | });
26 | }
27 |
28 | function fadeOut(): void {
29 | const elements = document.querySelectorAll(".fadeout") as NodeListOf;
30 | elements.forEach(element => {
31 | element.style.transition = "opacity 0.5s ease";
32 | element.style.opacity = "0";
33 | });
34 | }
35 |
36 | export const Fadeout = {
37 | init() {
38 |
39 | document.addEventListener("mousemove", resetTimer);
40 | document.addEventListener("mousedown", resetTimer);
41 | document.addEventListener("touchstart", resetTimer);
42 | document.addEventListener("touchmove", resetTimer);
43 | document.addEventListener("keydown", resetTimer);
44 | document.addEventListener("chat", resetTimerChat)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/webui/src/index.css:
--------------------------------------------------------------------------------
1 | @import 'tailwindcss';
2 |
3 | @config '../tailwind.config.cjs';
4 |
5 | /*
6 | The default border color has changed to `currentColor` in Tailwind CSS v4,
7 | so we've added these compatibility styles to make sure everything still
8 | looks the same as it did with Tailwind CSS v3.
9 |
10 | If we ever want to remove these styles, we need to add an explicit border
11 | color utility to any element that depends on these defaults.
12 | */
13 | @layer base {
14 | *,
15 | ::after,
16 | ::before,
17 | ::backdrop,
18 | ::file-selector-button {
19 | border-color: var(--color-gray-200, currentColor);
20 | }
21 | }
22 |
23 | .linkify {
24 | @apply underline text-blue-600 hover:text-blue-800 visited:text-purple-600
25 | }
26 |
--------------------------------------------------------------------------------
/webui/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import './index.css'
3 | import App from './App.vue'
4 | import router from './router'
5 |
6 | const app = createApp(App)
7 |
8 | app.use(router)
9 |
10 | app.mount('#app')
11 |
--------------------------------------------------------------------------------
/webui/src/router/index.ts:
--------------------------------------------------------------------------------
1 | import { createRouter, createWebHistory } from 'vue-router'
2 | import api from '../api'
3 | import config from '../config'
4 |
5 | const router = createRouter({
6 | history: createWebHistory(config.base()),
7 | routes: [
8 | {
9 | path: '/',
10 | name: 'Home',
11 | component: () => import('../views/HomeView.vue'),
12 | },
13 | {
14 | path: '/login/:token?',
15 | name: 'Login',
16 | props: true,
17 | component: () => import('../views/LoginView.vue'),
18 | },
19 | {
20 | path: '/social',
21 | name: 'Social',
22 | component: () => import('../views/SocialLoginView.vue'),
23 | },
24 | {
25 | path: '/fatal',
26 | name: 'FatalError',
27 | component: () => import('../views/FatalErrorView.vue'),
28 | },
29 | ],
30 | })
31 |
32 | interface Session {
33 | id: string
34 | auth: boolean
35 | user_id: string | null
36 | user_name: string
37 | }
38 |
39 | router.beforeEach(async (to) => {
40 | // deprecated session fallback
41 | let sess: Session = JSON.parse(window.localStorage.getItem('sess')!)
42 | if (sess) {
43 | console.log("session fallback");
44 | await api.session(sess.id)
45 | window.localStorage.removeItem('sess')
46 | }
47 | await api.connect(to.params.token)
48 | const auth = await api.isAuthenticated()
49 | if (!auth && to.name !== 'Login' && to.name !== 'Social') {
50 | return { name: 'Login' }
51 | }
52 | if (auth && to.name === 'Login') {
53 | return { name: 'Home' }
54 | }
55 | })
56 |
57 | export default router
58 |
--------------------------------------------------------------------------------
/webui/src/views/FatalErrorView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 500
6 |
7 |
8 |
Error
9 |
Please check the URL in the address bar and try again.
10 |
11 |
12 |
Retry
18 |
22 | Logout
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
37 |
--------------------------------------------------------------------------------
/webui/src/views/HomeView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
19 |
23 | {{
24 | State.chat_unread.value
25 | }}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
68 |
--------------------------------------------------------------------------------
/webui/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare module '*.vue' {
4 | import type { DefineComponent } from 'vue'
5 | const component: DefineComponent<{}, {}, any>
6 | export default component
7 | }
8 |
9 | declare const APP_VERSION: string;
10 |
--------------------------------------------------------------------------------
/webui/src/webcam.ts:
--------------------------------------------------------------------------------
1 | import Cropper from 'cropperjs'
2 | import 'cropperjs/dist/cropper.css'
3 | import { ref } from 'vue'
4 |
5 | let videoStream: MediaStream | undefined
6 | let cropper: Cropper | undefined
7 | let hvideo: HTMLVideoElement | null
8 |
9 | function getRoundedCanvas(sourceCanvas: HTMLCanvasElement | undefined) {
10 | if (!sourceCanvas)
11 | return
12 | const canvas = document.createElement('canvas')
13 | const ctx = canvas.getContext('2d')
14 | const width = sourceCanvas.width
15 | const height = sourceCanvas.height
16 |
17 | canvas.width = width
18 | canvas.height = height
19 | if (ctx) {
20 | ctx.imageSmoothingEnabled = true
21 | ctx.drawImage(sourceCanvas, 0, 0, width, height)
22 | ctx.globalCompositeOperation = 'destination-in'
23 | ctx.beginPath()
24 | ctx.arc(width / 2, height / 2, Math.min(width, height) / 2, 0, 2 * Math.PI, true)
25 | ctx.fill()
26 | }
27 | return canvas;
28 | }
29 | const constraintsVideo: any = {
30 | audio: false,
31 | video: {
32 | deviceId: undefined,
33 | },
34 | }
35 |
36 | export default {
37 | picture: ref(),
38 | preview: ref(false),
39 | deviceInfos: ref([]),
40 | deviceId: ref(undefined),
41 |
42 | video(video: HTMLVideoElement | null) {
43 | hvideo = video
44 | },
45 |
46 | async start() {
47 | try {
48 | constraintsVideo.video.deviceId = this.deviceId.value
49 | videoStream = await navigator.mediaDevices.getUserMedia(constraintsVideo)
50 | } catch (e) {
51 | console.error(`An error occurred: ${e}`)
52 | }
53 | this.deviceId.value = videoStream?.getVideoTracks()[0].getSettings().deviceId
54 | if (hvideo && videoStream) {
55 | hvideo.srcObject = videoStream
56 | hvideo.play()
57 | }
58 |
59 | this.picture.value = undefined
60 | this.preview.value = false
61 | this.deviceInfos.value = await navigator.mediaDevices.enumerateDevices()
62 | },
63 |
64 | stop() {
65 | videoStream?.getVideoTracks()[0].stop()
66 | },
67 |
68 | takePicture(canvas: HTMLCanvasElement | null, video: HTMLVideoElement | null) {
69 | if (!video || !canvas)
70 | return
71 |
72 | this.preview.value = true
73 | const context = canvas.getContext('2d')
74 | const width = 512
75 | const height = 512 * video.videoHeight / video.videoWidth
76 |
77 | canvas.setAttribute('width', String(width))
78 | canvas.setAttribute('height', String(height))
79 |
80 | context?.drawImage(video!, 0, 0, width, height)
81 | cropper = new Cropper(canvas, { aspectRatio: 1 })
82 | },
83 |
84 | savePicture() {
85 | const croppedCanvas = cropper?.getCroppedCanvas()
86 | const roundedCanvas = getRoundedCanvas(croppedCanvas)
87 | this.picture.value = roundedCanvas?.toDataURL('image/png')
88 | cropper?.destroy()
89 | },
90 | }
91 |
--------------------------------------------------------------------------------
/webui/src/webrtc_source.ts:
--------------------------------------------------------------------------------
1 | import api from './api'
2 |
3 | const pc_configuration: RTCConfiguration = {
4 | bundlePolicy: 'balanced',
5 | iceCandidatePoolSize: 0,
6 | iceServers: [],
7 | iceTransportPolicy: 'all',
8 | };
9 |
10 | export class WebRTCSource {
11 | private pc: RTCPeerConnection
12 | public audio: MediaStream | null
13 | public video: MediaStream | null
14 | public id: number
15 |
16 | constructor(id: number) {
17 | this.pc = new RTCPeerConnection(pc_configuration)
18 |
19 | this.audio = null
20 | this.video = null
21 | this.id = id
22 |
23 | this.pc.onicecandidate = (event) => {
24 | if (event.candidate)
25 | api.sdp_candidate(event.candidate, this.id)
26 | }
27 |
28 | this.pc.ontrack = (event) => {
29 | const track = event.track
30 |
31 | if (track.kind == 'audio') {
32 | this.audio = event.streams[0]
33 | console.log("WebRTCSource: audio track added")
34 | }
35 |
36 | if (track.kind == 'video') {
37 | this.video = event.streams[0]
38 | const video: HTMLVideoElement | null = document.querySelector('video#source' + this.id)
39 |
40 | if (!video) {
41 | return
42 | }
43 |
44 | video.srcObject = this.video
45 | video.play()
46 |
47 | console.log("WebRTCSource: video track added")
48 | }
49 | }
50 | }
51 |
52 | async setRemoteDescription(descr: any) {
53 | try {
54 | await this.pc.setRemoteDescription(descr)
55 |
56 | const answer = await this.pc.createAnswer()
57 | await this.pc.setLocalDescription(answer)
58 |
59 | await api.sdp_answer(answer, this.id)
60 |
61 | } catch (error) {
62 | console.error('Error setting remote description:', error)
63 | }
64 | }
65 |
66 | close() {
67 | this.pc.close()
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/webui/tailwind.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
4 | theme: {
5 | extend: {
6 | typography: {
7 | DEFAULT: {
8 | css: {
9 | lineHeight: '1.35rem'
10 | }
11 | }
12 | }
13 | },
14 | },
15 | plugins: [
16 | require('@tailwindcss/typography')
17 | ],
18 | }
19 |
--------------------------------------------------------------------------------
/webui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "moduleResolution": "Node",
7 | "strict": true,
8 | "jsx": "preserve",
9 | "resolveJsonModule": true,
10 | "isolatedModules": true,
11 | "esModuleInterop": true,
12 | "lib": ["ESNext", "DOM"],
13 | "skipLibCheck": true,
14 | "noEmit": true
15 | },
16 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
17 | "references": [{ "path": "./tsconfig.node.json" }]
18 | }
19 |
--------------------------------------------------------------------------------
/webui/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": ["vite.config.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/webui/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import vue from '@vitejs/plugin-vue'
3 | import { execSync } from 'child_process'
4 | import tailwindcss from "@tailwindcss/vite";
5 |
6 | const commitHash = execSync('git rev-parse --short HEAD')
7 | .toString();
8 |
9 | // https://vitejs.dev/config/
10 | export default defineConfig({
11 | plugins: [vue(), tailwindcss()],
12 | define: {
13 | APP_VERSION: JSON.stringify("v1.0.0-beta-" + commitHash)
14 | },
15 | server: {
16 | proxy: {
17 | '/api': { target: 'http://127.0.0.1:9999' },
18 | '/ws': { target: 'ws://127.0.0.1:9999' },
19 | }
20 | }
21 | })
22 |
--------------------------------------------------------------------------------