├── .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 | 18 | 19 | 20 | ### 🎥 Improved video layouts 21 | 22 | The new video layouts try to make better use of the available space: 23 | 24 | ![screenshot new video layouts](/vidconv.drawio.png) 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 | ![screenshot solo button](/solo_button.png) 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 | 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 | 17 | 18 | 19 | ### 🎥 Verbesserte Video-Layouts 20 | 21 | Die neuen Video Layouts versuchen die vorhandene Fläche besser auszunutzen: 22 | 23 | ![screenshot neue video layouts](/vidconv.drawio.png) 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 | ![screenshot solo button](/solo_button.png) 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 | 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 | ![Bildschirmfoto der Raumerstellung](/create_room.png) 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 | ![Bildschirmfoto der Raum-Links](/room_links.png) 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 | ![screenshot of room creation](/create_room.png) 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 | ![screenshot of room links](/room_links.png) 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 | 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 | 78 | 79 | 85 | -------------------------------------------------------------------------------- /webui/src/components/ErrorText.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 65 | -------------------------------------------------------------------------------- /webui/src/components/FooterLinks.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 9 | -------------------------------------------------------------------------------- /webui/src/components/Listeners.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 79 | -------------------------------------------------------------------------------- /webui/src/components/ReactionEmoji.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 50 | 51 | 74 | -------------------------------------------------------------------------------- /webui/src/components/RecButton.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 61 | -------------------------------------------------------------------------------- /webui/src/components/StudioNav.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 62 | -------------------------------------------------------------------------------- /webui/src/components/WebcamPhoto.vue: -------------------------------------------------------------------------------- 1 | 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 | 30 | 31 | 37 | -------------------------------------------------------------------------------- /webui/src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 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 | --------------------------------------------------------------------------------