diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml index 1b74e7006dc481..597242aa22790a 100644 --- a/.github/workflows/clang-format.yml +++ b/.github/workflows/clang-format.yml @@ -13,20 +13,8 @@ jobs: - name: Install clang format run: | - # gets us newer clang - sudo bash -c "cat >> /etc/apt/sources.list" << LLVMAPT - # 3.8 - deb http://apt.llvm.org/xenial/ llvm-toolchain-xenial-8 main - deb-src http://apt.llvm.org/xenial/ llvm-toolchain-xenial-8 main - LLVMAPT - - wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key|sudo apt-key add - - - sudo apt-get -qq update - - sudo apt-get install -y clang-format-8 - + sudo apt-get install -y clang-format-10 - name: Check the Formatting run: | ./formatcode.sh - ./CI/check-format.sh + ./CI/check-format.sh \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4f86883fc1d353..2e8cbb0e4808ac 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,19 +5,18 @@ on: paths-ignore: - '**.md' branches: - - master - tags: - - '*' + - caffeine pull_request: paths-ignore: - '**.md' branches: - - master + - caffeine env: MACOS_CEF_BUILD_VERSION: '4183' LINUX_CEF_BUILD_VERSION: '3770' CEF_VERSION: '75.1.16+g16a67c4+chromium-75.0.3770.100' + LIBCAFFEINE_VERSION: '0.6.3' jobs: macos64: @@ -137,13 +136,19 @@ jobs: make -j4 mkdir libcef_dll cd ../../ + - name: 'Install prerequisites: Libcaffeine' + shell: bash + run: | + curl -L -O https://github.com/caffeinetv/libcaffeine/releases/download/v${{ env.LIBCAFFEINE_VERSION }}/libcaffeine-v${{ env.LIBCAFFEINE_VERSION }}-macos.7z + brew install p7zip + 7z x libcaffeine-v${{ env.LIBCAFFEINE_VERSION }}-macos.7z -o${{ github.workspace }}/cmbuild - name: 'Configure' shell: bash run: | mkdir ./build cd ./build LEGACY_BROWSER="$(test "${{ env.MACOS_CEF_BUILD_VERSION }}" -le 3770 && echo "ON" || echo "OFF")" - cmake -DENABLE_UNIT_TESTS=YES -DENABLE_SPARKLE_UPDATER=ON -DDISABLE_PYTHON=ON -DCMAKE_OSX_DEPLOYMENT_TARGET=${{ env.MIN_MACOS_VERSION }} -DQTDIR="/tmp/obsdeps" -DSWIGDIR="/tmp/obsdeps" -DDepsPath="/tmp/obsdeps" -DVLCPath="${{ github.workspace }}/cmbuild/vlc-${{ env.VLC_VERSION }}" -DENABLE_VLC=ON -DBUILD_BROWSER=ON -DBROWSER_LEGACY=$LEGACY_BROWSER -DWITH_RTMPS=ON -DCEF_ROOT_DIR="${{ github.workspace }}/cmbuild/cef_binary_${{ env.MACOS_CEF_BUILD_VERSION }}_macosx64" .. + cmake -DENABLE_UNIT_TESTS=YES -DENABLE_SPARKLE_UPDATER=ON -DDISABLE_PYTHON=ON -DCMAKE_OSX_DEPLOYMENT_TARGET=${{ env.MIN_MACOS_VERSION }} -DQTDIR="/tmp/obsdeps" -DSWIGDIR="/tmp/obsdeps" -DDepsPath="/tmp/obsdeps" -DVLCPath="${{ github.workspace }}/cmbuild/vlc-${{ env.VLC_VERSION }}" -DENABLE_VLC=ON -DBUILD_BROWSER=ON -DBROWSER_LEGACY=$LEGACY_BROWSER -DWITH_RTMPS=ON -DCEF_ROOT_DIR="${{ github.workspace }}/cmbuild/cef_binary_${{ env.MACOS_CEF_BUILD_VERSION }}_macosx64" -DLIBCAFFEINE_DIR="${{ github.workspace }}/cmbuild/libcaffeine-v${{ env.LIBCAFFEINE_VERSION }}-macos" .. - name: 'Build' shell: bash working-directory: ${{ github.workspace }}/build @@ -346,7 +351,7 @@ jobs: path: ./release/*.dmg ubuntu64: name: 'Linux/Ubuntu 64-bit' - runs-on: [ubuntu-latest] + runs-on: [ubuntu-18.04] steps: - name: 'Checkout' uses: actions/checkout@v2.3.3 @@ -556,12 +561,16 @@ jobs: run: | curl -kL https://cdn-fastly.obsproject.com/downloads/cef_binary_${{ env.CEF_VERSION }}_windows64_minimal.zip -f --retry 5 -o cef.zip 7z x cef.zip -o"${{ github.workspace }}/cmbuild" + - name: 'Installing prerequisites: Libcaffeine' + run: | + curl -kLO https://github.com/caffeinetv/libcaffeine/releases/download/v${{ env.LIBCAFFEINE_VERSION }}/libcaffeine-v${{ env.LIBCAFFEINE_VERSION }}-windows.7z + 7z x libcaffeine-v${{ env.LIBCAFFEINE_VERSION }}-windows.7z -o"${{ github.workspace }}/cmbuild" - name: 'Configure' run: | mkdir ./build mkdir ./build64 cd ./build64 - cmake -G"${{ env.CMAKE_GENERATOR }}" -A"x64" -DCMAKE_SYSTEM_VERSION="${{ env.CMAKE_SYSTEM_VERSION }}" -DBUILD_BROWSER=true -DCOMPILE_D3D12_HOOK=true -DVLCPath="${{ github.workspace }}/cmbuild/vlc" -DDepsPath="${{ github.workspace }}/cmbuild/deps/win64" -DQTDIR="${{ github.workspace }}/cmbuild/QT/${{ env.QT_VERSION }}/msvc2019_64" -DENABLE_VLC=ON -DCEF_ROOT_DIR="${{ github.workspace }}/cmbuild/cef_binary_${{ env.CEF_VERSION }}_windows64_minimal" -DCOPIED_DEPENDENCIES=FALSE -DCOPY_DEPENDENCIES=TRUE -DVIRTUALCAM_GUID=${{ env.VIRTUALCAM-GUID }} .. + cmake -G"${{ env.CMAKE_GENERATOR }}" -A"x64" -DCMAKE_SYSTEM_VERSION="${{ env.CMAKE_SYSTEM_VERSION }}" -DBUILD_BROWSER=true -DCOMPILE_D3D12_HOOK=true -DVLCPath="${{ github.workspace }}/cmbuild/vlc" -DDepsPath="${{ github.workspace }}/cmbuild/deps/win64" -DQTDIR="${{ github.workspace }}/cmbuild/QT/${{ env.QT_VERSION }}/msvc2019_64" -DENABLE_VLC=ON -DCEF_ROOT_DIR="${{ github.workspace }}/cmbuild/cef_binary_${{ env.CEF_VERSION }}_windows64_minimal" -DCOPIED_DEPENDENCIES=FALSE -DCOPY_DEPENDENCIES=TRUE -DVIRTUALCAM_GUID=${{ env.VIRTUALCAM-GUID }} -DLIBCAFFEINE_DIR="${{ github.workspace }}/cmbuild/libcaffeine-v${{ env.LIBCAFFEINE_VERSION }}-windows" .. - name: 'Build' run: msbuild /m /p:Configuration=RelWithDebInfo .\build64\obs-studio.sln - name: 'Package' @@ -668,12 +677,16 @@ jobs: run: | curl -kL https://cdn-fastly.obsproject.com/downloads/cef_binary_${{ env.CEF_VERSION }}_windows32_minimal.zip -f --retry 5 -o cef.zip 7z x cef.zip -o"${{ github.workspace }}/cmbuild" + - name: 'Installing prerequisites: Libcaffeine' + run: | + curl -kLO https://github.com/caffeinetv/libcaffeine/releases/download/v${{ env.LIBCAFFEINE_VERSION }}/libcaffeine-v${{ env.LIBCAFFEINE_VERSION }}-windows.7z + 7z x libcaffeine-v${{ env.LIBCAFFEINE_VERSION }}-windows.7z -o"${{ github.workspace }}/cmbuild" - name: 'Configure' run: | mkdir ./build mkdir ./build32 cd ./build32 - cmake -G"${{ env.CMAKE_GENERATOR }}" -A"Win32" -DCMAKE_SYSTEM_VERSION="${{ env.CMAKE_SYSTEM_VERSION }}" -DENABLE_VLC=ON -DBUILD_BROWSER=true -DCOMPILE_D3D12_HOOK=true -DVLCPath="${{ github.workspace }}/cmbuild/vlc" -DDepsPath="${{ github.workspace }}/cmbuild/deps/win32" -DQTDIR="${{ github.workspace }}/cmbuild/QT/${{ env.QT_VERSION }}/msvc2019" -DCEF_ROOT_DIR="${{ github.workspace }}/cmbuild/cef_binary_${{ env.CEF_VERSION }}_windows32_minimal" -DCOPIED_DEPENDENCIES=FALSE -DCOPY_DEPENDENCIES=TRUE -DVIRTUALCAM_GUID=${{ env.VIRTUALCAM-GUID }} .. + cmake -G"${{ env.CMAKE_GENERATOR }}" -A"Win32" -DCMAKE_SYSTEM_VERSION="${{ env.CMAKE_SYSTEM_VERSION }}" -DENABLE_VLC=ON -DBUILD_BROWSER=true -DCOMPILE_D3D12_HOOK=true -DVLCPath="${{ github.workspace }}/cmbuild/vlc" -DDepsPath="${{ github.workspace }}/cmbuild/deps/win32" -DQTDIR="${{ github.workspace }}/cmbuild/QT/${{ env.QT_VERSION }}/msvc2019" -DCEF_ROOT_DIR="${{ github.workspace }}/cmbuild/cef_binary_${{ env.CEF_VERSION }}_windows32_minimal" -DCOPIED_DEPENDENCIES=FALSE -DCOPY_DEPENDENCIES=TRUE -DVIRTUALCAM_GUID=${{ env.VIRTUALCAM-GUID }} -DLIBCAFFEINE_DIR="${{ github.workspace }}/cmbuild/libcaffeine-v${{ env.LIBCAFFEINE_VERSION }}-windows" .. - name: 'Build' run: msbuild /m /p:Configuration=RelWithDebInfo .\build32\obs-studio.sln - name: 'Package' diff --git a/.gitignore b/.gitignore index 747a7c65a9fcfb..bf3092b2bc23d2 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,11 @@ .ninja* .dirstamp +#dependencies +/libcaffeine-v0.6.1-macos/ +/vlc-3.0.8/ +/CI/scripts/macos/Brewfile.lock.json + #xcode *.xcodeproj/ diff --git a/CI/full-build-macos.sh b/CI/full-build-macos.sh index 80a73fc5b137f7..eda6c9cc7d29a0 100755 --- a/CI/full-build-macos.sh +++ b/CI/full-build-macos.sh @@ -22,7 +22,7 @@ # # Environment Variables (optional): # MACOS_DEPS_VERSION : Pre-compiled macOS dependencies version -# CEF_BUILD_VERSION : Chromium Embedded Framework version +# MACOS_CEF_BUILD_VERSION : Chromium Embedded Framework version # VLC_VERISON : VLC version # SPARKLE_VERSION : Sparke Framework version # BUILD_DIR : Alternative directory to build OBS in @@ -41,20 +41,22 @@ BUILD_DIR="${BUILD_DIR:-build}" BUILD_CONFIG=${BUILD_CONFIG:-RelWithDebInfo} CI_SCRIPTS="${CHECKOUT_DIR}/CI/scripts/macos" CI_WORKFLOW="${CHECKOUT_DIR}/.github/workflows/main.yml" -CI_CEF_VERSION=$(cat ${CI_WORKFLOW} | sed -En "s/[ ]+CEF_BUILD_VERSION: '([0-9]+)'/\1/p") +CI_MACOS_CEF_VERSION=$(cat ${CI_WORKFLOW} | sed -En "s/[ ]+MACOS_CEF_BUILD_VERSION: '([0-9]+)'/\1/p") CI_DEPS_VERSION=$(cat ${CI_WORKFLOW} | sed -En "s/[ ]+MACOS_DEPS_VERSION: '([0-9\-]+)'/\1/p") CI_VLC_VERSION=$(cat ${CI_WORKFLOW} | sed -En "s/[ ]+VLC_VERSION: '([0-9\.]+)'/\1/p") CI_SPARKLE_VERSION=$(cat ${CI_WORKFLOW} | sed -En "s/[ ]+SPARKLE_VERSION: '([0-9\.]+)'/\1/p") CI_QT_VERSION=$(cat ${CI_WORKFLOW} | sed -En "s/[ ]+QT_VERSION: '([0-9\.]+)'/\1/p" | head -1) CI_MIN_MACOS_VERSION=$(cat ${CI_WORKFLOW} | sed -En "s/[ ]+MIN_MACOS_VERSION: '([0-9\.]+)'/\1/p") +CI_LIBCAFFEINE_VERSION=$(cat ${CI_WORKFLOW} | sed -En "s/[ ]+LIBCAFFEINE_VERSION: '([0-9\.]+)'/\1/p") NPROC="${NPROC:-$(sysctl -n hw.ncpu)}" BUILD_DEPS=( - "obs-deps ${MACOS_DEPS_VERSION:-${CI_DEPS_VERSION}}" - "qt-deps ${QT_VERSION:-${CI_QT_VERSION}} ${MACOS_DEPS_VERSION:-${CI_DEPS_VERSION}}" - "cef ${CEF_BUILD_VERSION:-${CI_CEF_VERSION}}" + "obs_deps ${MACOS_DEPS_VERSION:-${CI_DEPS_VERSION}}" + "qt_deps ${QT_VERSION:-${CI_QT_VERSION}} ${MACOS_DEPS_VERSION:-${CI_DEPS_VERSION}}" + "cef ${MACOS_CEF_BUILD_VERSION:-${CI_MACOS_CEF_VERSION}}" "vlc ${VLC_VERSION:-${CI_VLC_VERSION}}" "sparkle ${SPARKLE_VERSION:-${CI_SPARKLE_VERSION}}" + "libcaffeine ${LIBCAFFEINE_VERSION:-${CI_LIBCAFFEINE_VERSION}}" ) if [ -n "${TERM-}" ]; then @@ -165,7 +167,7 @@ check_ccache() { info "${CCACHE_STATUS}" } -install_obs-deps() { +install_obs_deps() { hr "Setting up pre-built macOS OBS dependencies v${1}" ensure_dir ${DEPS_BUILD_DIR} step "Download..." @@ -174,7 +176,7 @@ install_obs-deps() { tar -xf ./macos-deps-${1}.tar.gz -C /tmp } -install_qt-deps() { +install_qt_deps() { hr "Setting up pre-built dependency QT v${1}" ensure_dir ${DEPS_BUILD_DIR} step "Download..." @@ -208,6 +210,16 @@ install_sparkle() { fi } +install_libcaffeine() { + hr "Setting up dependency libcaffeine v${1}" + ensure_dir ${DEPS_BUILD_DIR} + step "Download..." + ${CURLCMD} --progress-bar -L -C - -O https://github.com/caffeinetv/libcaffeine/releases/download/v${1}/libcaffeine-v${1}-macos.7z + step "Unpack ..." + brew list p7zip || brew install p7zip + 7za x libcaffeine-v${1}-macos.7z +} + install_cef() { hr "Building dependency CEF v${1}" ensure_dir ${DEPS_BUILD_DIR} @@ -218,7 +230,7 @@ install_cef() { cd ./cef_binary_${1}_macosx64 step "Fix tests..." sed -i '.orig' '/add_subdirectory(tests\/ceftests)/d' ./CMakeLists.txt - sed -i '.orig' 's/"'$(test "${CEF_BUILD_VERSION:-${CI_CEF_VERSION}}" -le 3770 && echo "10.9" || echo "10.10")'"/"'${MIN_MACOS_VERSION:-${CI_MIN_MACOS_VERSION}}'"/' ./cmake/cef_variables.cmake + sed -i '.orig' 's/"'$(test "${MACOS_CEF_BUILD_VERSION:-${CI_MACOS_CEF_VERSION}}" -le 3770 && echo "10.9" || echo "10.10")'"/"'${MIN_MACOS_VERSION:-${CI_MIN_MACOS_VERSION}}'"/' ./cmake/cef_variables.cmake ensure_dir ./build step "Run CMAKE..." cmake \ @@ -277,11 +289,13 @@ configure_obs_build() { -DSWIGDIR="/tmp/obsdeps" \ -DDepsPath="/tmp/obsdeps" \ -DVLCPath="${DEPS_BUILD_DIR}/vlc-${VLC_VERSION:-${CI_VLC_VERSION}}" \ + -DENABLE_VLC=ON \ -DBUILD_BROWSER=ON \ - -DBROWSER_LEGACY="$(test "${CEF_BUILD_VERSION:-${CI_CEF_VERSION}}" -le 3770 && echo "ON" || echo "OFF")" \ + -DBROWSER_LEGACY="$(test "${MACOS_CEF_BUILD_VERSION:-${CI_MACOS_CEF_VERSION}}" -le 3770 && echo "ON" || echo "OFF")" \ -DWITH_RTMPS=ON \ - -DCEF_ROOT_DIR="${DEPS_BUILD_DIR}/cef_binary_${CEF_BUILD_VERSION:-${CI_CEF_VERSION}}_macosx64" \ + -DCEF_ROOT_DIR="${DEPS_BUILD_DIR}/cef_binary_${MACOS_CEF_BUILD_VERSION:-${CI_MACOS_CEF_VERSION}}_macosx64" \ -DCMAKE_BUILD_TYPE="${BUILD_CONFIG}" \ + -DLIBCAFFEINE_DIR="${DEPS_BUILD_DIR}/libcaffeine-v${LIBCAFFEINE_VERSION:-${CI_LIBCAFFEINE_VERSION}}-macos" \ .. } @@ -329,7 +343,7 @@ bundle_dylibs() { ./OBS.app/Contents/PlugIns/text-freetype2.so ./OBS.app/Contents/PlugIns/obs-outputs.so ) - if ! [ "${CEF_BUILD_VERSION:-${CI_CEF_VERSION}}" -le 3770 ]; then + if ! [ "${MACOS_CEF_BUILD_VERSION:-${CI_MACOS_CEF_VERSION}}" -le 3770 ]; then ${CI_SCRIPTS}/app/dylibbundler -cd -of -a ./OBS.app -q -f \ -s ./OBS.app/Contents/MacOS \ -s "${DEPS_BUILD_DIR}/sparkle/Sparkle.framework" \ @@ -371,7 +385,7 @@ install_frameworks() { hr "Adding Chromium Embedded Framework" step "Copy Framework..." - cp -R "${DEPS_BUILD_DIR}/cef_binary_${CEF_BUILD_VERSION:-${CI_CEF_VERSION}}_macosx64/Release/Chromium Embedded Framework.framework" ./OBS.app/Contents/Frameworks/ + cp -R "${DEPS_BUILD_DIR}/cef_binary_${MACOS_CEF_BUILD_VERSION:-${CI_MACOS_CEF_VERSION}}_macosx64/Release/Chromium Embedded Framework.framework" ./OBS.app/Contents/Frameworks/ chown -R $(whoami) ./OBS.app/Contents/Frameworks/ } @@ -395,7 +409,7 @@ prepare_macos_bundle() { cp rundir/${BUILD_CONFIG}/bin/obs ./OBS.app/Contents/MacOS cp rundir/${BUILD_CONFIG}/bin/obs-ffmpeg-mux ./OBS.app/Contents/MacOS cp rundir/${BUILD_CONFIG}/bin/libobsglad.0.dylib ./OBS.app/Contents/MacOS - if ! [ "${CEF_BUILD_VERSION:-${CI_CEF_VERSION}}" -le 3770 ]; then + if ! [ "${MACOS_CEF_BUILD_VERSION:-${CI_MACOS_CEF_VERSION}}" -le 3770 ]; then cp -R "rundir/${BUILD_CONFIG}/bin/OBS Helper.app" "./OBS.app/Contents/Frameworks/OBS Helper.app" cp -R "rundir/${BUILD_CONFIG}/bin/OBS Helper (GPU).app" "./OBS.app/Contents/Frameworks/OBS Helper (GPU).app" cp -R "rundir/${BUILD_CONFIG}/bin/OBS Helper (Plugin).app" "./OBS.app/Contents/Frameworks/OBS Helper (Plugin).app" @@ -524,7 +538,7 @@ codesign_bundle() { codesign --force --options runtime --sign "${CODESIGN_IDENT}" "./OBS.app/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libswiftshader_libEGL.dylib" codesign --force --options runtime --sign "${CODESIGN_IDENT}" "./OBS.app/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libGLESv2.dylib" codesign --force --options runtime --sign "${CODESIGN_IDENT}" "./OBS.app/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libswiftshader_libGLESv2.dylib" - if ! [ "${CEF_BUILD_VERSION:-${CI_CEF_VERSION}}" -le 3770 ]; then + if ! [ "${MACOS_CEF_BUILD_VERSION:-${CI_MACOS_CEF_VERSION}}" -le 3770 ]; then codesign --force --options runtime --sign "${CODESIGN_IDENT}" "./OBS.app/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libvk_swiftshader.dylib" fi @@ -540,7 +554,7 @@ codesign_bundle() { codesign --force --options runtime --entitlements "${CI_SCRIPTS}/app/entitlements.plist" --sign "${CODESIGN_IDENT}" --deep ./OBS.app echo -n "${COLOR_RESET}" - if ! [ "${CEF_BUILD_VERSION:-${CI_CEF_VERSION}}" -le 3770 ]; then + if ! [ "${MACOS_CEF_BUILD_VERSION:-${CI_MACOS_CEF_VERSION}}" -le 3770 ]; then step "Code-sign CEF helper apps..." echo -n "${COLOR_ORANGE}" codesign --force --options runtime --sign "${CODESIGN_IDENT}" --deep "./OBS.app/Contents/Frameworks/OBS Helper.app" @@ -578,7 +592,7 @@ codesign_image() { } ## BUILD FROM SOURCE META FUNCTION ## -full-build-macos() { +full_build_macos() { if [ -n "${SKIP_BUILD}" ]; then step "Skipping full build"; return; fi if [ ! -n "${SKIP_DEPS}" ]; then @@ -664,7 +678,7 @@ print_usage() { exit 0 } -obs-build-main() { +obs_build_main() { ensure_dir ${CHECKOUT_DIR} check_macos_version step "Fetching OBS tags..." @@ -701,7 +715,7 @@ obs-build-main() { esac done - full-build-macos + full_build_macos bundle_macos codesign_bundle package_macos @@ -711,4 +725,4 @@ obs-build-main() { cleanup } -obs-build-main $* +obs_build_main $* diff --git a/CI/scripts/macos/Brewfile b/CI/scripts/macos/Brewfile index eff533552dacb5..c7b8c062de3e15 100644 --- a/CI/scripts/macos/Brewfile +++ b/CI/scripts/macos/Brewfile @@ -2,4 +2,4 @@ tap "akeru-inc/tap" brew "cmake" brew "freetype" brew "cmocka" -brew "akeru-inc/tap/xcnotary" \ No newline at end of file +brew "akeru-inc/tap/xcnotary" diff --git a/CMakeLists.txt b/CMakeLists.txt index 426d0628b9effd..05c9fd5372d21b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -253,6 +253,18 @@ if(NOT INSTALLER_RUN) endif() find_package(Qt5Widgets ${FIND_MODE}) + + option(BUILD_CAFFEINE "Enables streaming to Caffeine.tv" ON) + if (BUILD_CAFFEINE) + find_package(libcaffeine) + if (LIBCAFFEINE_FOUND) + set(CAFFEINE_ENABLED TRUE) + else() + set(CAFFEINE_ENABLED FALSE) + endif() + else() + set(CAFFEINE_ENABLED FALSE) + endif() endif() add_subdirectory(deps) diff --git a/README.rst b/README.rst index 682b3abe13e71a..33b55dd78a1086 100644 --- a/README.rst +++ b/README.rst @@ -39,6 +39,103 @@ Quick Links - Bug Tracker: https://github.com/obsproject/obs-studio/issues +Building from source +-------------------- + + Clone the repository and submodules + .. code-block:: text + + git clone --recursive https://github.com/obsproject/obs-studio.git + +**Windows:** + + Install the following prerequisites: + + 1. `Qt 5.14.1 `_ + + 2. `Cmake `_ + + 3. Pre-built windows dependencies for VS2017 https://obsproject.com/downloads/dependencies2017.zip + 4. `Visual Studio 2019 `_ + 5. `LLVM `_ + 6. `Embedded chrome browser library `_ version 08/29/2018 - CEF 3.3440.1806.g65046b7 / Chromium 68.0.3440.106 + + After installing the `prerequisites `_ .Create the following environment variables: + + #. **QTDIR** - Path pointing Qt 5.14.1 msvc2017_64 folder + + #. **obsInstallerTempDir** - Empty directory path + + #. **DepsPath** - Path to pre-built windows dependencies win64/include + + #. **LIBCAFFEINE_DIR** - Path to `prebuilt libcaffeine `_ folder + + #. **CEF_ROOT_DIR** - Path to the embedded chrome browser library + + Run the automated build script: ``build.bat [OPTION]`` + + .. csv-table:: + :header: "Option", "Usage" + :widths: 20, 80 + + "*-help*", "To display the supported options." + "*-check*", "To verify project prerequisites are set." + "*-build*", "To build 64 bit version of obs-studio." + "*-package*", "To build package." + + +**Mac:** + + Install following prerequisites: + + - Homebrew + - `Qt `_ + + Build steps: + + Open Terminal + + 1. Install FFmpeg + + .. code-block:: text + + brew tap homebrew-ffmpeg/ffmpeg + + brew install homebrew-ffmpeg/ffmpeg/ffmpeg --with-srt + + 2. Set environment variable for Qt + + .. code-block:: text + + export QTDIR="path/to/Qt" + + export DYLID_FRAMEWORK_PATH="path/to/Qt/5.14.1/clang_64/lib" + + 3. Install cmake + + .. code-block:: text + + brew install cmake + + 4. Change directory obs-studio directory + + .. code-block:: text + + mkdir build + + cd build + + cmake .. & make + + 5. After it built successfully then run the app + + .. code-block:: text + + cd rundir/RelWithDebInfo/bin + + ./obs + + Contributing ------------ diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index ae881a3f657bf1..31bd03f7fb2c6a 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -137,7 +137,41 @@ elseif(UNIX) endif() endif() -if(BROWSER_AVAILABLE_INTERNAL) +if(CAFFEINE_ENABLED) + find_package(libcaffeine REQUIRED) + find_package(Threads REQUIRED) + add_definitions(${THREADS_DEFINITIONS}) + + if(WIN32) + set(caffeine_PLATFORM_DEPS + shlwapi.lib + w32-pthreads) + endif() + + include_directories( + ${LIBCAFFEINE_INCLUDE_DIR} + ) + set(caffeine_HEADERS + auth-caffeine.hpp + ) + set(caffeine_SOURCES + auth-caffeine.cpp + ) + list(APPEND obs_PLATFORM_SOURCES + ${caffeine_SOURCES} + ) + list(APPEND obs_PLATFORM_HEADERS + ${caffeine_HEADERS} + ) + set(obs_PLATFORM_LIBRARIES + ${obs_PLATFORM_LIBRARIES} + ${caffeine_PLATFORM_DEPS} + libcaffeine + ${THREADS_LIBRARIES}) +endif() + +if(CAFFEINE_ENABLED OR BROWSER_AVAILABLE_INTERNAL) + add_definitions(-DAUTH_ENABLED) list(APPEND obs_PLATFORM_SOURCES obf.c auth-oauth.cpp @@ -150,7 +184,9 @@ if(BROWSER_AVAILABLE_INTERNAL) window-dock-browser.hpp window-extra-browsers.hpp ) +endif() +if(BROWSER_AVAILABLE_INTERNAL) if(TWITCH_ENABLED) list(APPEND obs_PLATFORM_SOURCES auth-twitch.cpp @@ -369,6 +405,22 @@ set(obs_UI set(obs_QRC forms/obs.qrc) +if(CAFFEINE_ENABLED) + list(APPEND obs_UI + forms/CaffeineSignIn.ui + forms/CaffeinePanel.ui + ) + list(APPEND obs_HEADERS + window-caffeine.hpp + ) + list(APPEND obs_SOURCES + window-caffeine.cpp + ) + list(APPEND obs_QRC + forms/Caffeine.qrc + ) +endif() + qt5_wrap_ui(obs_UI_HEADERS ${obs_UI}) qt5_add_resources(obs_QRC_SOURCES ${obs_QRC}) diff --git a/UI/auth-base.cpp b/UI/auth-base.cpp index 89c30e2c057079..08b1a42463c9cd 100644 --- a/UI/auth-base.cpp +++ b/UI/auth-base.cpp @@ -4,6 +4,9 @@ #include #include +struct QCef; +extern QCef *cef; + struct AuthInfo { Auth::Def def; Auth::create_cb create; @@ -39,6 +42,30 @@ Auth::Type Auth::AuthType(const std::string &service) return Type::None; } +bool Auth::IsKeyHidden(const std::string &service) +{ + for (auto &a : authDefs) { + if (service.find(a.def.service) != std::string::npos) { + return a.def.key_hidden; + } + } + + return false; +} + +bool Auth::CanAuthService(const std::string &service) +{ + switch (Auth::AuthType(service)) { + case Auth::Type::Custom: + return true; + case Auth::Type::OAuth_StreamKey: + return !!cef; + case Auth::Type::None: + default: + return false; + } +} + void Auth::Load() { OBSBasic *main = OBSBasic::Get(); diff --git a/UI/auth-base.hpp b/UI/auth-base.hpp index f683ce1f18f629..7b2c2f88fe59eb 100644 --- a/UI/auth-base.hpp +++ b/UI/auth-base.hpp @@ -24,14 +24,12 @@ class Auth : public QObject { }; public: - enum class Type { - None, - OAuth_StreamKey, - }; + enum class Type { None, OAuth_StreamKey, Custom }; struct Def { std::string service; Type type; + bool key_hidden; }; typedef std::function()> create_cb; @@ -48,6 +46,8 @@ class Auth : public QObject { static std::shared_ptr Create(const std::string &service); static Type AuthType(const std::string &service); + static bool IsKeyHidden(const std::string &service); + static bool CanAuthService(const std::string &service); static void Load(); static void Save(); diff --git a/UI/auth-caffeine.cpp b/UI/auth-caffeine.cpp new file mode 100644 index 00000000000000..575f5fa7c144c7 --- /dev/null +++ b/UI/auth-caffeine.cpp @@ -0,0 +1,334 @@ +#include "auth-caffeine.hpp" + +#include +#include + +#include + +#include "window-basic-main.hpp" +#include "window-caffeine.hpp" + +#include "ui-config.h" + +#include "ui_CaffeineSignIn.h" + +static Auth::Def caffeineDef = {"Caffeine", Auth::Type::Custom, true}; +const int otpLength = 6; +// Get the Caffeine URL default or staging +static const char *getCaffeineURL = getenv("LIBCAFFEINE_DOMAIN") == NULL + ? "caffeine.tv" + : getenv("LIBCAFFEINE_DOMAIN"); + +/* ------------------------------------------------------------------------- */ + +static int addFonts() +{ + QFontDatabase::addApplicationFont( + ":/caffeine/fonts/Poppins-Regular.ttf"); + QFontDatabase::addApplicationFont(":/caffeine/fonts/Poppins-Bold.ttf"); + QFontDatabase::addApplicationFont(":/caffeine/fonts/Poppins-Light.ttf"); + QFontDatabase::addApplicationFont( + ":/caffeine/fonts/Poppins-ExtraLight.ttf"); + return 0; +} + +CaffeineAuth::CaffeineAuth(const Def &d) : OAuthStreamKey(d) +{ + UNUSED_PARAMETER(d); + instance = caff_createInstance(); +} + +CaffeineAuth::~CaffeineAuth() +{ + caff_freeInstance(&instance); +} + +bool CaffeineAuth::GetChannelInfo() +try { + key_ = refresh_token; + + if (caff_isSignedIn(instance)) { + username = caff_getUsername(instance); + } else { + switch (caff_refreshAuth(instance, refresh_token.c_str())) { + case caff_ResultSuccess: + username = caff_getUsername(instance); + break; + case caff_ResultRefreshTokenRequired: + case caff_ResultInfoIncorrect: + throw ErrorInfo(Str("Caffeine.Auth.Unauthorized"), + Str("Caffeine.Auth.IncorrectRefresh")); + default: + throw ErrorInfo(Str("Caffeine.Auth.Failed"), + Str("Caffeine.Auth.SigninFailed")); + } + } + + OBSBasic *main = OBSBasic::Get(); + obs_service_t *service = main->GetService(); + obs_data_t *settings = obs_service_get_settings(service); + obs_data_set_string(settings, "username", username.c_str()); + obs_data_release(settings); + + return true; +} catch (ErrorInfo info) { + QString title = QTStr("Auth.ChannelFailure.Title"); + QString text = QTStr("Auth.ChannelFailure.Text") + .arg(service(), info.message.c_str(), + info.error.c_str()); + + QMessageBox::warning(OBSBasic::Get(), title, text); + + blog(LOG_WARNING, "%s: %s: %s", __FUNCTION__, info.message.c_str(), + info.error.c_str()); + return false; +} + +void CaffeineAuth::SaveInternal() +{ + OBSBasic *main = OBSBasic::Get(); + config_set_string(main->Config(), service(), "Username", + username.c_str()); + if (uiLoaded) { + config_set_string(main->Config(), service(), "DockState", + main->saveState().toBase64().constData()); + } + OAuthStreamKey::SaveInternal(); +} + +static inline std::string get_config_str(OBSBasic *main, const char *section, + const char *name) +{ + const char *val = config_get_string(main->Config(), section, name); + return val ? val : ""; +} + +bool CaffeineAuth::LoadInternal() +{ + OBSBasic *main = OBSBasic::Get(); + username = get_config_str(main, service(), "Username"); + firstLoad = false; + return OAuthStreamKey::LoadInternal(); +} + +class CaffeineChat : public OBSDock { +public: + inline CaffeineChat() : OBSDock() {} +}; + +void CaffeineAuth::LoadUI() +{ + if (uiLoaded) + return; + if (!GetChannelInfo()) + return; + /* TODO: Chat */ + + OBSBasic *main = OBSBasic::Get(); + // Panel + panelDock = QSharedPointer( + new CaffeineInfoPanel(this, instance)) + .dynamicCast(); + + // Set min size of panel so it doesn't get clipped when docked + panelDock->setMinimumSize(268, 270); + if (firstLoad) { + panelDock->setVisible(true); + } else { + const char *dockStateStr = config_get_string( + main->Config(), service(), "DockState"); + QByteArray dockState = + QByteArray::fromBase64(QByteArray(dockStateStr)); + main->restoreState(dockState); + } + uiLoaded = true; + return; +} + +void CaffeineAuth::OnStreamConfig() +{ + OBSBasic *main = OBSBasic::Get(); + obs_service_t *service = main->GetService(); + obs_data_t *data = obs_service_get_settings(service); + QSharedPointer panel2 = + panelDock.dynamicCast(); + obs_data_set_string(data, "broadcast_title", + panel2->getTitle().c_str()); + obs_data_set_int(data, "rating", + static_cast(panel2->getRating())); + obs_service_update(service, data); + obs_data_release(data); +} + +bool CaffeineAuth::RetryLogin() +{ + std::shared_ptr ptr = Login(OBSBasic::Get()); + return ptr != nullptr; +} + +void CaffeineAuth::TryAuth(Ui::CaffeineSignInDialog *ui, QDialog *dialog, + std::string &passwordForOtp) +{ + std::string username = ui->usernameEdit->text().toStdString(); + std::string otp; + std::string password; + if (passwordForOtp.empty()) { + password = ui->passwordEdit->text().toStdString(); + } else { + otp = ui->passwordEdit->text().toStdString(); + password = passwordForOtp; + } + + auto result = caff_signIn(instance, username.c_str(), password.c_str(), + otp.c_str()); + switch (result) { + case caff_ResultSuccess: + refresh_token = caff_getRefreshToken(instance); + dialog->accept(); + return; + case caff_ResultMfaOtpRequired: + ui->messageLabel->setText( + QString(R"(%1 )") + .arg(QTStr("Caffeine.Auth.EnterCode"), + getCaffeineURL)); + ui->usernameEdit->hide(); + passwordForOtp = ui->passwordEdit->text().toStdString(); + ui->passwordEdit->clear(); + ui->passwordEdit->setPlaceholderText( + Str("Caffeine.Auth.VerificationPlaceholder")); + ui->signInButton->setText(Str("Caffeine.Auth.Continue")); + ui->newUserFooter->hide(); + ui->forgotPasswordLabel->setOpenExternalLinks(false); + ui->forgotPasswordLabel->setText( + QString(R"(

+ %1
%3

)") + .arg(QTStr("Caffeine.Auth.HavingProblems"), + getCaffeineURL, + QTStr("Caffeine.Auth.SendNewCode"))); + + // Disable the signInButton upfront + ui->signInButton->setEnabled(false); + ui->passwordEdit->setMaxLength(otpLength); + // Enable the signInButton when user has entered 6 digit opt + connect(ui->passwordEdit, &QLineEdit::textChanged, + [=](const QString &enteredOtp) { + if (enteredOtp.length() == otpLength) { + ui->signInButton->setEnabled(true); + } else { + ui->signInButton->setEnabled(false); + } + }); + return; + case caff_ResultMfaOtpIncorrect: + ui->passwordEdit->clear(); + ui->messageLabel->setText( + Str("Caffeine.Auth.IncorrectVerification")); + return; + case caff_ResultUsernameRequired: + ui->messageLabel->setText( + Str("Caffeine.Auth.UsernameRequired")); + return; + case caff_ResultPasswordRequired: + ui->messageLabel->setText( + Str("Caffeine.Auth.PasswordRequired")); + return; + case caff_ResultInfoIncorrect: + ui->messageLabel->setText(Str("Caffeine.Auth.IncorrectInfo")); + return; + case caff_ResultLegalAcceptanceRequired: + ui->messageLabel->setText( + Str("Caffeine.Auth.TosAcceptanceRequired")); + return; + case caff_ResultEmailVerificationRequired: + ui->messageLabel->setText( + Str("Caffeine.Auth.EmailVerificationRequired")); + return; + case caff_ResultInternetDisconnected: + ui->messageLabel->setText( + Str("Caffeine.InternetDisconnected.Text")); + return; + case caff_ResultFailure: + default: + ui->messageLabel->setText(Str("Caffeine.Auth.SigninFailed")); + return; + } +} + +std::shared_ptr CaffeineAuth::Login(QWidget *parent) +{ + static int once = addFonts(); + UNUSED_PARAMETER(once); + const auto flags = Qt::WindowTitleHint | Qt::WindowSystemMenuHint | + Qt::WindowCloseButtonHint; + + QDialog dialog(parent, flags); + auto ui = new Ui::CaffeineSignInDialog; + ui->setupUi(&dialog); + QIcon icon(":/caffeine/images/CaffeineLogo.svg"); + ui->logo->setPixmap(icon.pixmap(76, 66)); + + // Set up text and point href depending on the env var + ui->forgotPasswordLabel->setText(QString(R"(

+ forgot something?
reset your password

)") + .arg(getCaffeineURL)); + ui->newUserFooter->setText( + QString(R"(New to Caffeine? sign up)") + .arg(getCaffeineURL)); + + // Don't highlight text boxes + for (auto edit : dialog.findChildren()) { + edit->setAttribute(Qt::WA_MacShowFocusRect, false); + } + + ui->messageLabel->clear(); + + std::shared_ptr auth = + std::make_shared(caffeineDef); + + std::string origPassword; + connect(ui->signInButton, &QPushButton::clicked, + [&](bool) { auth->TryAuth(ui, &dialog, origPassword); }); + + // Only used for One-time-password "resend email" link. resending the + // email is just attempting the sign-in without one-time password + // included + connect(ui->forgotPasswordLabel, &QLabel::linkActivated, + [&](const QString &) { + // Do this only for OTP + if (!ui->newUserFooter->isVisible()) { + auto username = + ui->usernameEdit->text().toStdString(); + caff_signIn(auth->instance, username.c_str(), + origPassword.c_str(), nullptr); + } + }); + + if (dialog.exec() == QDialog::Rejected) + return nullptr; + + if (auth->GetChannelInfo()) + return auth; + + return nullptr; +} + +std::string CaffeineAuth::GetUsername() +{ + return this->username; +} + +static std::shared_ptr CreateCaffeineAuth() +{ + return std::make_shared(caffeineDef); +} + +static void DeleteCookies() {} + +void RegisterCaffeineAuth() +{ + OAuth::RegisterOAuth(caffeineDef, CreateCaffeineAuth, + CaffeineAuth::Login, DeleteCookies); +} diff --git a/UI/auth-caffeine.hpp b/UI/auth-caffeine.hpp new file mode 100644 index 00000000000000..245535fd623777 --- /dev/null +++ b/UI/auth-caffeine.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "auth-oauth.hpp" +#include "caffeine.h" +#include "window-dock.hpp" + +class CaffeineChat; +class QWidget; +class QDialog; + +namespace Ui { +class CaffeineSignInDialog; +} + +class CaffeineAuth : public OAuthStreamKey { + Q_OBJECT + caff_InstanceHandle instance; + + bool uiLoaded = false; + QSharedPointer panelDock; + QSharedPointer chat; + QSharedPointer chatMenu; + + std::string username; + + void TryAuth(Ui::CaffeineSignInDialog *ui, QDialog *dialog, + std::string &origPassword); + virtual bool RetryLogin() override; + + virtual void SaveInternal() override; + virtual bool LoadInternal() override; + + bool GetChannelInfo(); + + virtual void LoadUI() override; + + virtual void OnStreamConfig() override; + +public: + CaffeineAuth(const Def &d); + virtual ~CaffeineAuth(); + + static std::shared_ptr Login(QWidget *parent); + + std::string GetUsername(); +}; diff --git a/UI/auth-oauth.cpp b/UI/auth-oauth.cpp index a27c49c1749327..47533abcdf3430 100644 --- a/UI/auth-oauth.cpp +++ b/UI/auth-oauth.cpp @@ -16,15 +16,18 @@ using namespace json11; +#ifdef BROWSER_AVAILABLE #include extern QCef *cef; extern QCefCookieManager *panel_cookies; +#endif /* ------------------------------------------------------------------------- */ OAuthLogin::OAuthLogin(QWidget *parent, const std::string &url, bool token) : QDialog(parent), get_token(token) { +#ifdef BROWSER_AVAILABLE if (!cef) { return; } @@ -61,18 +64,23 @@ OAuthLogin::OAuthLogin(QWidget *parent, const std::string &url, bool token) QVBoxLayout *topLayout = new QVBoxLayout(this); topLayout->addWidget(cefWidget); topLayout->addLayout(bottomLayout); +#endif } OAuthLogin::~OAuthLogin() { +#ifdef BROWSER_AVAILABLE delete cefWidget; +#endif } int OAuthLogin::exec() { +#ifdef BROWSER_AVAILABLE if (cefWidget) { return QDialog::exec(); } +#endif return QDialog::Rejected; } diff --git a/UI/auth-oauth.hpp b/UI/auth-oauth.hpp index 7cf7dc403e37ac..b7f07cbe354df1 100644 --- a/UI/auth-oauth.hpp +++ b/UI/auth-oauth.hpp @@ -6,12 +6,16 @@ #include "auth-base.hpp" +#ifdef BROWSER_AVAILABLE class QCefWidget; +#endif class OAuthLogin : public QDialog { Q_OBJECT +#ifdef BROWSER_AVAILABLE QCefWidget *cefWidget = nullptr; +#endif QString code; bool get_token = false; bool fail = false; diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 09205f7b2f529a..a894467e4bd0fe 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -87,6 +87,8 @@ ShowInMultiview="Show in Multiview" VerticalLayout="Vertical Layout" Group="Group" DoNotShowAgain="Do not show again" +Required="Required" +Optional="Optional" Default="(Default)" Calculating="Calculating..." Fullscreen="Fullscreen" @@ -169,7 +171,7 @@ Basic.AutoConfig.VideoPage.FPS.PreferHighRes="Either 60 or 30, but prefer high r Basic.AutoConfig.VideoPage.CanvasExplanation="Note: The canvas (base) resolution is not necessarily the same as the resolution you will stream or record with. Your actual stream/recording resolution may be scaled down from the canvas resolution to reduce resource usage or bitrate requirements." Basic.AutoConfig.StreamPage="Stream Information" Basic.AutoConfig.StreamPage.SubTitle="Please enter your stream information" -Basic.AutoConfig.StreamPage.ConnectAccount="Connect Account (recommended)" +Basic.AutoConfig.StreamPage.ConnectAccount="Connect Account (%1)" Basic.AutoConfig.StreamPage.DisconnectAccount="Disconnect Account" Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Title="Disconnect Account?" Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Text="This change will apply immediately. Are you sure you want to disconnect your account?" @@ -700,6 +702,7 @@ Basic.Settings.General.MultiviewLayout.Horizontal.Extended.Top="Horizontal, Top Basic.Settings.Stream="Stream" Basic.Settings.Stream.StreamType="Stream Type" Basic.Settings.Stream.Custom.UseAuthentication="Use authentication" +Basic.Settings.Stream.Custom.SignedInAs="Signed in as" Basic.Settings.Stream.Custom.Username="Username" Basic.Settings.Stream.Custom.Password="Password" Basic.Settings.Stream.BandwidthTestMode="Enable Bandwidth Test Mode" @@ -1079,3 +1082,34 @@ ContextBar.MediaControls.PlaylistNext="Next in Playlist" ContextBar.MediaControls.PlaylistPrevious="Previous in Playlist" ContextBar.MediaControls.MediaProperties="Media Properties" ContextBar.MediaControls.BlindSeek="Media Seek Widget" + +# Caffeine +Caffeine.Auth.Unauthorized="Unauthorized" +Caffeine.Auth.Failed="Failed" +Caffeine.Auth.UsernameRequired="Username is required" +Caffeine.Auth.PasswordRequired="Password is required" +Caffeine.Auth.IncorrectInfo="The username or password is incorrect." +Caffeine.Auth.EnterCode="Enter the two-factor authentication code that was emailed to you." +Caffeine.Auth.SendNewCode="send a new code" +Caffeine.Auth.HavingProblems="having problems?" +Caffeine.Auth.ResendEmail="Resend email." +Caffeine.Auth.VerificationPlaceholder="Enter code" +Caffeine.Auth.Continue="Continue" +Caffeine.Auth.IncorrectVerification="An incorrect code was entered." +Caffeine.Auth.IncorrectRefresh="You have been signed out of Caffeine." +Caffeine.Auth.TosAcceptanceRequired="You must accept the Caffeine Terms of Use before broadcasting" +Caffeine.Auth.EmailVerificationRequired="Verify your email address before broadcasting" +Caffeine.Auth.SigninFailed="There was an error signing in. Try again." +Caffeine.Dock="Caffeine" +Caffeine.Dock.RatingAndTitle="Set your broadcast rating and title:" +Caffeine.Dock.UpdateRatingAndTitle="Update rating and title" +Caffeine.Dock.ViewOnWeb="View on Web" +Caffeine.Dock.ViewOnWebDescription="View on web to see credits, viewers,\nand chat with your audience." +Caffeine.Title="LIVE on Caffeine!" +Caffeine.Rating.None="0+" +Caffeine.Rating.SeventeenPlus="17+" +Caffeine.InternetDisconnected.Title="Internet Connection Lost" +Caffeine.InternetDisconnected.Text="You have lost your connection. Please reconnect" +Caffeine.SystemOverload.Title="System Overload" +Caffeine.SystemOverload.Text="System under too much load. Broadcast quality may be degraded." +Caffeine.SystemOverload.CheckBox.Text="Message will not show again during this app session" \ No newline at end of file diff --git a/UI/forms/Caffeine.qrc b/UI/forms/Caffeine.qrc new file mode 100644 index 00000000000000..8043fe283c700a --- /dev/null +++ b/UI/forms/Caffeine.qrc @@ -0,0 +1,9 @@ + + + images/CaffeineLogo.svg + fonts/Poppins-Bold.ttf + fonts/Poppins-Regular.ttf + fonts/Poppins-Light.ttf + fonts/Poppins-ExtraLight.ttf + + diff --git a/UI/forms/CaffeinePanel.ui b/UI/forms/CaffeinePanel.ui new file mode 100644 index 00000000000000..9e99e126f17487 --- /dev/null +++ b/UI/forms/CaffeinePanel.ui @@ -0,0 +1,548 @@ + + + CaffeinePanel + + + + 0 + 0 + 388 + 251 + + + + + + + true + + + QDockWidget::DockWidgetClosable|QDockWidget::DockWidgetMovable|QDockWidget::DockWidgetFloatable + + + + Caffeine + + + + + 0 + 0 + + + + + + + + 0 + + + 24 + + + 24 + + + 24 + + + 24 + + + + + true + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + Segoe UI + + + + + + + Set your broadcasts rating and title: + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 8 + + + + + + + + + 0 + 0 + + + + + 12 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 1 + 0 + + + + + 76 + 32 + + + + + Segoe UI + true + + + + + + + + Test + + + + + + + + + 3 + 0 + + + + + 0 + 32 + + + + + Segoe UI + + + + + + + LIVE on Caffeine! + + + false + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + + Segoe UI + + + + PointingHandCursor + + + + + + Update rating and title + + + false + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 20 + 5 + + + + + + + + + + + + + + Qt::Horizontal + + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 16 + + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 0 + 0 + + + + + + + + true + + + + 0 + 0 + + + + + 120 + 32 + + + + + 16777215 + 32 + + + + + Segoe UI + + + + PointingHandCursor + + + + + + View on web + + + false + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 0 + 0 + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 8 + + + + + + + + + 0 + 0 + + + + + Segoe UI + + + + + + + <html><body><p>View on web to see credits, viewers, and chat with your audience.</p></body></html> + + + Qt::AlignCenter + + + true + + + + + + + + + + + + + + + + + + diff --git a/UI/forms/CaffeineSignIn.ui b/UI/forms/CaffeineSignIn.ui new file mode 100644 index 00000000000000..b2ab4a552d2643 --- /dev/null +++ b/UI/forms/CaffeineSignIn.ui @@ -0,0 +1,645 @@ + + + CaffeineSignInDialog + + + Qt::ApplicationModal + + + + 0 + 0 + 672 + 691 + + + + + 0 + 0 + + + + + + + + + 0 + 0 + 0 + + + + + + + 255 + 255 + 255 + + + + + + + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 0 + 159 + 224 + + + + + + + 0 + 159 + 224 + + + + + + + + + 0 + 0 + 0 + + + + + + + 255 + 255 + 255 + + + + + + + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 0 + 159 + 224 + + + + + + + 0 + 159 + 224 + + + + + + + + + 0 + 0 + 0 + + + + + + + 255 + 255 + 255 + + + + + + + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 0 + 159 + 224 + + + + + + + 0 + 159 + 224 + + + + + + + + Caffeine + + + * { + color: black; + font-size: 14px; + font-family: Poppins, "Segoe UI", Sans; + background-color: white; +} + +QLineEdit { + padding: 5px 20px; + font-family: "Poppins Light"; + border-radius: 0px; + border: 1px solid #f2f2f2; + border-top: 4px solid #f2f2f2; +} + +QPushButton { + font-size: 24px; + background-color: rgb(0, 159, 224); + color:white; + border-radius: 40px; + border: 0px solid black; +} + +QPushButton::disabled { + color: rgb(116, 116, 116); + background-color: rgb(231, 231, 239); +} + +QPushButton::focus { + border-radius: 40px; + border: 1px solid black; + outline: 0px solid black; +} + +QPushButton::hover { + color: white; + background-color: rgb(0, 0, 0); + border: 1px solid black; +} + +QLabel#forgotPasswordLabel { + line-height: 75%; + font-family: 'Poppins Light'; + font-size: 14px; +} + + + + true + + + + 0 + + + QLayout::SetFixedSize + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + 195 + + + 195 + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 32 + + + + + + + + + + + 76 + 66 + + + + + 76 + 66 + + + + + + + :/caffeine/images/CaffeineLogo.svg + + + true + + + Qt::AlignCenter + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 28 + + + + + + + + + + + Poppins + -1 + + + + <p style="line-height: 92%; font-size: 32px; text-align:center; font-family: 'Poppins ExtraLight'">Sign in to<br/>Caffeine</p> + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + + 0 + 0 + + + + + 282 + 42 + + + + + 282 + 42 + + + + font-family: 'Poppins Light'; + font-size: 14px; + color: #8b8b8b; + + + This is where messages will appear. There can be up to 2 lines + + + Qt::AlignCenter + + + true + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + + 0 + 0 + + + + + 280 + 0 + + + + + + + username + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 8 + + + + + + + + QLineEdit::Password + + + password + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 64 + + + + + + + + + 280 + 80 + + + + + 280 + 80 + + + + PointingHandCursor + + + sign in + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 14 + + + + + + + + padding-bottom: 5px; + + + Qt::AlignCenter + + + true + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 69 + + + + + + + + + 0 + 62 + + + + + 16777215 + 62 + + + + background-color: rgb(0, 159, 224); color: white; padding: 18px 0px 16px 0px; + + + Qt::AlignCenter + + + true + + + + + + + + + + diff --git a/UI/forms/OBSBasicSettings.ui b/UI/forms/OBSBasicSettings.ui index fd53049d407f7f..5bd19a84decd5b 100644 --- a/UI/forms/OBSBasicSettings.ui +++ b/UI/forms/OBSBasicSettings.ui @@ -970,6 +970,22 @@ + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 170 + 19 + + + + @@ -1259,6 +1275,26 @@ + + + + QFrame::NoFrame + + + Basic.Settings.Stream.Custom.SignedInAs + + + + + + + QFrame::NoFrame + + + SomeUser + + + diff --git a/UI/forms/fonts/Poppins-Bold.ttf b/UI/forms/fonts/Poppins-Bold.ttf new file mode 100644 index 00000000000000..1170ea9d81eaf0 Binary files /dev/null and b/UI/forms/fonts/Poppins-Bold.ttf differ diff --git a/UI/forms/fonts/Poppins-ExtraLight.ttf b/UI/forms/fonts/Poppins-ExtraLight.ttf new file mode 100644 index 00000000000000..20fe902c462cdf Binary files /dev/null and b/UI/forms/fonts/Poppins-ExtraLight.ttf differ diff --git a/UI/forms/fonts/Poppins-License.txt b/UI/forms/fonts/Poppins-License.txt new file mode 100644 index 00000000000000..88294838696e15 --- /dev/null +++ b/UI/forms/fonts/Poppins-License.txt @@ -0,0 +1,43 @@ +Copyright (c) 2014, Indian Type Foundry (info@indiantypefoundry.com). + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + +—————————————————————————————- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +—————————————————————————————- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. + +DEFINITIONS +“Font Software” refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. + +“Reserved Font Name” refers to any names specified as such after the copyright statement(s). + +“Original Version” refers to the collection of Font Software components as distributed by the Copyright Holder(s). + +“Modified Version” refers to any derivative made by adding to, deleting, or substituting—in part or in whole—any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. + +“Author” refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. + +5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/UI/forms/fonts/Poppins-Light.ttf b/UI/forms/fonts/Poppins-Light.ttf new file mode 100644 index 00000000000000..1e74b8a2780787 Binary files /dev/null and b/UI/forms/fonts/Poppins-Light.ttf differ diff --git a/UI/forms/fonts/Poppins-Regular.ttf b/UI/forms/fonts/Poppins-Regular.ttf new file mode 100644 index 00000000000000..850d03e68a16f5 Binary files /dev/null and b/UI/forms/fonts/Poppins-Regular.ttf differ diff --git a/UI/forms/images/CaffeineLogo.svg b/UI/forms/images/CaffeineLogo.svg new file mode 100644 index 00000000000000..26d99859d1af25 --- /dev/null +++ b/UI/forms/images/CaffeineLogo.svg @@ -0,0 +1,19 @@ + + + + path-1 + Created with Sketch. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/UI/obs-app.cpp b/UI/obs-app.cpp index 8e421a27a1c10a..762545c1c6d594 100644 --- a/UI/obs-app.cpp +++ b/UI/obs-app.cpp @@ -1518,10 +1518,10 @@ QString OBSTranslator::translate(const char *context, const char *sourceText, { const char *out = nullptr; if (!App()->TranslateString(sourceText, &out)) - return QString(sourceText); + if (!App()->TranslateString(disambiguation, &out)) + return QString(sourceText); UNUSED_PARAMETER(context); - UNUSED_PARAMETER(disambiguation); UNUSED_PARAMETER(n); return QT_UTF8(out); } diff --git a/UI/ui-config.h.in b/UI/ui-config.h.in index 0fcc4845ade1de..7cb6b8d96fdbac 100644 --- a/UI/ui-config.h.in +++ b/UI/ui-config.h.in @@ -24,4 +24,6 @@ #define RESTREAM_CLIENTID "@RESTREAM_CLIENTID@" #define RESTREAM_HASH 0x@RESTREAM_HASH@ +#define CAFFEINE_ENABLED @CAFFEINE_ENABLED@ + #define DEFAULT_THEME "Dark" diff --git a/UI/ui-validation.cpp b/UI/ui-validation.cpp index e441cbb6a49125..082a5ac26780ad 100644 --- a/UI/ui-validation.cpp +++ b/UI/ui-validation.cpp @@ -1,5 +1,6 @@ #include "ui-validation.hpp" +#include "ui-config.h" #include #include #include @@ -75,7 +76,13 @@ UIValidation::StreamSettingsConfirmation(QWidget *parent, OBSService service) return StreamSettingsAction::ContinueStream; QString msg; - +#if CAFFEINE_ENABLED + if (hasStreamUrl) { + return StreamSettingsAction::ContinueStream; + } else { + msg = QTStr("Basic.Settings.Stream.MissingUrl"); + } +#else if (!hasStreamUrl && !hasStreamKey) { msg = QTStr("Basic.Settings.Stream.MissingUrlAndApiKey"); } else if (!hasStreamKey) { @@ -83,6 +90,7 @@ UIValidation::StreamSettingsConfirmation(QWidget *parent, OBSService service) } else { msg = QTStr("Basic.Settings.Stream.MissingUrl"); } +#endif QMessageBox messageBox(parent); messageBox.setWindowTitle( diff --git a/UI/window-basic-auto-config-test.cpp b/UI/window-basic-auto-config-test.cpp index a5ba9bda0319fb..ad8d5ace45e2b6 100644 --- a/UI/window-basic-auto-config-test.cpp +++ b/UI/window-basic-auto-config-test.cpp @@ -281,6 +281,13 @@ void AutoConfigTestPage::TestBandwidthThread() OBSOutput output = obs_output_create(output_type, "test_stream", nullptr, nullptr); obs_output_release(output); + uint32_t flags = obs_output_get_flags(output); + if (flags & OBS_OUTPUT_HARDWARE_ENCODING_DISABLED) + wiz->hardwareEncodingAvailable = false; + if (flags & OBS_OUTPUT_BANDWIDTH_TEST_DISABLED) { + QMetaObject::invokeMethod(this, "NextStage"); + return; + } obs_output_update(output, output_settings); const char *audio_codec = obs_output_get_supported_audio_codecs(output); @@ -1113,6 +1120,8 @@ void AutoConfigTestPage::FinalizeResults() new QLabel(scaleRes, ui->finishPage)); form->addRow(newLabel("Basic.Settings.Video.FPS"), new QLabel(fpsStr, ui->finishPage)); + + QTimer::singleShot(0, [this]() { wiz->adjustSize(); }); } #define STARTING_SEPARATOR \ diff --git a/UI/window-basic-auto-config.cpp b/UI/window-basic-auto-config.cpp index 03cf065b7c61d5..909ba840a61303 100644 --- a/UI/window-basic-auto-config.cpp +++ b/UI/window-basic-auto-config.cpp @@ -7,6 +7,7 @@ #include "window-basic-main.hpp" #include "qt-wrappers.hpp" #include "obs-app.hpp" +#include "ui-config.h" #include "url-push-button.hpp" #include "ui_AutoConfigStartPage.h" @@ -15,14 +16,11 @@ #ifdef BROWSER_AVAILABLE #include -#include "auth-oauth.hpp" #endif -struct QCef; -struct QCefCookieManager; - -extern QCef *cef; -extern QCefCookieManager *panel_cookies; +#ifdef AUTH_ENABLED +#include "auth-oauth.hpp" +#endif #define wiz reinterpret_cast(wizard()) @@ -188,9 +186,7 @@ AutoConfigVideoPage::~AutoConfigVideoPage() int AutoConfigVideoPage::nextId() const { - return wiz->type == AutoConfig::Type::Recording - ? AutoConfig::TestPage - : AutoConfig::StreamPage; + return AutoConfig::TestPage; } bool AutoConfigVideoPage::validatePage() @@ -231,6 +227,17 @@ bool AutoConfigVideoPage::validatePage() break; } + if (wiz->service != AutoConfig::Service::Twitch && wiz->bandwidthTest) { + QMessageBox::StandardButton button; +#define WARNING_TEXT(x) QTStr("Basic.AutoConfig.StreamPage.StreamWarning." x) + button = OBSMessageBox::question(this, WARNING_TEXT("Title"), + WARNING_TEXT("Text")); +#undef WARNING_TEXT + + if (button == QMessageBox::No) + return false; + } + return true; } @@ -312,7 +319,7 @@ bool AutoConfigStreamPage::isComplete() const int AutoConfigStreamPage::nextId() const { - return AutoConfig::TestPage; + return AutoConfig::VideoPage; } inline bool AutoConfigStreamPage::IsCustomService() const @@ -340,7 +347,9 @@ bool AutoConfigStreamPage::validatePage() obs_service_release(service); int bitrate = 10000; - if (!ui->doBandwidthTest->isChecked()) { + bool doBandwidthTest = ui->doBandwidthTest->isChecked() && + ui->doBandwidthTest->isEnabled(); + if (!doBandwidthTest) { bitrate = ui->bitrate->value(); wiz->idealBitrate = bitrate; } @@ -358,7 +367,7 @@ bool AutoConfigStreamPage::validatePage() wiz->server = QT_TO_UTF8(ui->server->currentData().toString()); } - wiz->bandwidthTest = ui->doBandwidthTest->isChecked(); + wiz->bandwidthTest = doBandwidthTest; wiz->startingBitrate = (int)obs_data_get_int(settings, "bitrate"); wiz->idealBitrate = wiz->startingBitrate; wiz->regionUS = ui->regionUS->isChecked(); @@ -366,8 +375,10 @@ bool AutoConfigStreamPage::validatePage() wiz->regionAsia = ui->regionAsia->isChecked(); wiz->regionOther = ui->regionOther->isChecked(); wiz->serviceName = QT_TO_UTF8(ui->service->currentText()); - if (ui->preferHardware) - wiz->preferHardware = ui->preferHardware->isChecked(); + if (ui->preferHardware) { + wiz->preferHardware = ui->preferHardware->isChecked() && + ui->preferHardware->isEnabled(); + } wiz->key = QT_TO_UTF8(ui->key->text()); if (!wiz->customServer) { @@ -379,17 +390,6 @@ bool AutoConfigStreamPage::validatePage() wiz->service = AutoConfig::Service::Other; } - if (wiz->service != AutoConfig::Service::Twitch && wiz->bandwidthTest) { - QMessageBox::StandardButton button; -#define WARNING_TEXT(x) QTStr("Basic.AutoConfig.StreamPage.StreamWarning." x) - button = OBSMessageBox::question(this, WARNING_TEXT("Title"), - WARNING_TEXT("Text")); -#undef WARNING_TEXT - - if (button == QMessageBox::No) - return false; - } - return true; } @@ -406,7 +406,7 @@ void AutoConfigStreamPage::on_show_clicked() void AutoConfigStreamPage::OnOAuthStreamKeyConnected() { -#ifdef BROWSER_AVAILABLE +#ifdef AUTH_ENABLED OAuthStreamKey *a = reinterpret_cast(auth.get()); if (a) { @@ -431,14 +431,14 @@ void AutoConfigStreamPage::OnAuthConnected() std::string service = QT_TO_UTF8(ui->service->currentText()); Auth::Type type = Auth::AuthType(service); - if (type == Auth::Type::OAuth_StreamKey) { + if (type != Auth::Type::None) { OnOAuthStreamKeyConnected(); } } void AutoConfigStreamPage::on_connectAccount_clicked() { -#ifdef BROWSER_AVAILABLE +#ifdef AUTH_ENABLED std::string service = QT_TO_UTF8(ui->service->currentText()); OAuth::DeleteCookies(service); @@ -467,17 +467,22 @@ void AutoConfigStreamPage::on_disconnectAccount_clicked() OBSBasic *main = OBSBasic::Get(); - main->auth.reset(); - auth.reset(); + // Remove the auth here if caffeine account is not associated + if (ui->service->currentText() != "Caffeine") { + main->auth.reset(); + auth.reset(); + } std::string service = QT_TO_UTF8(ui->service->currentText()); -#ifdef BROWSER_AVAILABLE +#ifdef AUTH_ENABLED OAuth::DeleteCookies(service); #endif - ui->streamKeyWidget->setVisible(true); - ui->streamKeyLabel->setVisible(true); + bool hidden_key = Auth::IsKeyHidden(service); + + ui->streamKeyWidget->setVisible(!hidden_key); + ui->streamKeyLabel->setVisible(!hidden_key); ui->connectAccount2->setVisible(true); ui->disconnectAccount->setVisible(false); ui->key->setText(""); @@ -489,11 +494,6 @@ void AutoConfigStreamPage::on_useStreamKey_clicked() UpdateCompleted(); } -static inline bool is_auth_service(const std::string &service) -{ - return Auth::AuthType(service) != Auth::Type::None; -} - void AutoConfigStreamPage::ServiceChanged() { bool showMore = ui->service->currentData().toInt() == @@ -508,24 +508,59 @@ void AutoConfigStreamPage::ServiceChanged() ui->disconnectAccount->setVisible(false); -#ifdef BROWSER_AVAILABLE - if (cef) { - if (lastService != service.c_str()) { - bool can_auth = is_auth_service(service); - int page = can_auth ? (int)Section::Connect - : (int)Section::StreamKey; - - ui->stackedWidget->setCurrentIndex(page); - ui->streamKeyWidget->setVisible(true); - ui->streamKeyLabel->setVisible(true); - ui->connectAccount2->setVisible(can_auth); - auth.reset(); - - if (lastService.isEmpty()) - lastService = service.c_str(); +#ifdef AUTH_ENABLED + bool can_auth = Auth::CanAuthService(service); + bool hidden_auth = Auth::IsKeyHidden(service); + QString connectString = + QTStr("Basic.AutoConfig.StreamPage.ConnectAccount") + .arg(hidden_auth ? QTStr("Required") + : QTStr("Optional")); + ui->connectAccount->setText(connectString); + ui->connectAccount2->setText(connectString); + + const char *service_id = wiz->customServer ? "rtmp_custom" + : "rtmp_common"; + OBSData settings = obs_data_create(); + obs_data_release(settings); + + if (!wiz->customServer) + obs_data_set_string(settings, "service", service.c_str()); + + OBSService tService = obs_service_create(service_id, "temp_service", + settings, nullptr); + uint32_t flags = + obs_get_output_flags(obs_service_get_output_type(tService)); + obs_service_release(tService); + if (ui->preferHardware) { + if (flags & OBS_OUTPUT_HARDWARE_ENCODING_DISABLED) { + ui->preferHardware->setDisabled(true); + ui->preferHardware->setVisible(false); + } else { + ui->preferHardware->setDisabled(false); + ui->preferHardware->setVisible(true); } + } + if (flags & OBS_OUTPUT_BANDWIDTH_TEST_DISABLED) { + ui->doBandwidthTest->setDisabled(true); + ui->doBandwidthTest->setVisible(false); } else { - ui->connectAccount2->setVisible(false); + ui->doBandwidthTest->setDisabled(false); + ui->doBandwidthTest->setVisible(true); + } + + if (lastService != service.c_str()) { + int page = can_auth || hidden_auth ? (int)Section::Connect + : (int)Section::StreamKey; + ui->useStreamKey->setVisible(!hidden_auth); + + ui->stackedWidget->setCurrentIndex(page); + ui->streamKeyWidget->setVisible(true); + ui->streamKeyLabel->setVisible(true); + ui->connectAccount2->setVisible(can_auth); + auth.reset(); + + if (lastService.isEmpty()) + lastService = service.c_str(); } #else ui->connectAccount2->setVisible(false); @@ -562,7 +597,7 @@ void AutoConfigStreamPage::ServiceChanged() ui->bitrateLabel->setHidden(testBandwidth); ui->bitrate->setHidden(testBandwidth); -#ifdef BROWSER_AVAILABLE +#ifdef AUTH_ENABLED OBSBasic *main = OBSBasic::Get(); if (!!main->auth && @@ -686,11 +721,14 @@ void AutoConfigStreamPage::LoadServices(bool showAll) 0, QTStr("Basic.AutoConfig.StreamPage.Service.Custom"), QVariant((int)ListOpt::Custom)); - if (!lastService.isEmpty()) { - int idx = ui->service->findText(lastService); - if (idx != -1) - ui->service->setCurrentIndex(idx); + int idx = -1; + if (lastService.isEmpty()) { + idx = ui->service->findText("Twitch"); + } else { + idx = ui->service->findText(lastService); } + if (idx != -1) + ui->service->setCurrentIndex(idx); obs_properties_destroy(props); diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 2f25a33ed86058..387e93b99ab52b 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -188,6 +188,7 @@ void assignDockToggle(QDockWidget *dock, QAction *action) } extern void RegisterTwitchAuth(); +extern void RegisterCaffeineAuth(); extern void RegisterRestreamAuth(); OBSBasic::OBSBasic(QWidget *parent) @@ -201,6 +202,9 @@ OBSBasic::OBSBasic(QWidget *parent) #if TWITCH_ENABLED RegisterTwitchAuth(); #endif +#if CAFFEINE_ENABLED + RegisterCaffeineAuth(); +#endif #if RESTREAM_ENABLED RegisterRestreamAuth(); #endif @@ -6450,7 +6454,7 @@ void OBSBasic::on_settingsButton_clicked() void OBSBasic::on_actionHelpPortal_triggered() { - QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QUrl url = QUrl("https://caffeine.custhelp.com", QUrl::TolerantMode); QDesktopServices::openUrl(url); } @@ -8067,6 +8071,9 @@ QAction *OBSBasic::AddDockWidget(QDockWidget *dock) { QAction *action = ui->viewMenuDocks->addAction(dock->windowTitle()); action->setCheckable(true); + if (dock->isVisible()) { + action->setChecked(true); + } assignDockToggle(dock, action); extraDocks.push_back(dock); @@ -8089,6 +8096,16 @@ QAction *OBSBasic::AddDockWidget(QDockWidget *dock) return action; } +void OBSBasic::RemoveCaffeineDockWidget(QDockWidget *dock) +{ + for (auto &it : ui->viewMenuDocks->actions()) { + if (it->text() == dock->windowTitle()) { + ui->viewMenuDocks->removeAction(it); + return; + } + } +} + OBSBasic *OBSBasic::Get() { return reinterpret_cast(App()->GetMainWindow()); diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index 4f0ad6b3a14ecb..2c6d95e4a7043c 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -827,6 +827,7 @@ private slots: void CreateFiltersWindow(obs_source_t *source); QAction *AddDockWidget(QDockWidget *dock); + void RemoveCaffeineDockWidget(QDockWidget *dock); static OBSBasic *Get(); diff --git a/UI/window-basic-settings-stream.cpp b/UI/window-basic-settings-stream.cpp index 2189ed5a8e25cd..64697f2c48ea0b 100644 --- a/UI/window-basic-settings-stream.cpp +++ b/UI/window-basic-settings-stream.cpp @@ -10,14 +10,15 @@ #ifdef BROWSER_AVAILABLE #include -#include "auth-oauth.hpp" #endif -struct QCef; -struct QCefCookieManager; +#ifdef AUTH_ENABLED +#include "auth-oauth.hpp" +#endif -extern QCef *cef; -extern QCefCookieManager *panel_cookies; +#if CAFFEINE_ENABLED +#include "auth-caffeine.hpp" +#endif enum class ListOpt : int { ShowAll = 1, @@ -39,6 +40,8 @@ void OBSBasicSettings::InitStreamPage() ui->connectAccount2->setVisible(false); ui->disconnectAccount->setVisible(false); ui->bandwidthTestEnable->setVisible(false); + ui->authSignedInAs->setVisible(false); + ui->authSignedInAsLabel->setVisible(false); ui->twitchAddonDropdown->setVisible(false); ui->twitchAddonLabel->setVisible(false); @@ -104,14 +107,13 @@ void OBSBasicSettings::LoadStream1Settings() const char *service = obs_data_get_string(settings, "service"); const char *server = obs_data_get_string(settings, "server"); const char *key = obs_data_get_string(settings, "key"); + const char *username = obs_data_get_string(settings, "username"); if (strcmp(type, "rtmp_custom") == 0) { ui->service->setCurrentIndex(0); ui->customServer->setText(server); bool use_auth = obs_data_get_bool(settings, "use_auth"); - const char *username = - obs_data_get_string(settings, "username"); const char *password = obs_data_get_string(settings, "password"); ui->authUsername->setText(QT_UTF8(username)); @@ -120,9 +122,16 @@ void OBSBasicSettings::LoadStream1Settings() } else { int idx = ui->service->findText(service); if (idx == -1) { - if (service && *service) + if (service && *service) { + // Insert placeholder for unrecognized service ui->service->insertItem(1, service); - idx = 1; + idx = 1; + } else { + // Default to twitch or first non-custom service + idx = ui->service->findText("Twitch"); + if (idx == -1) + idx = 1; + } } ui->service->setCurrentIndex(idx); @@ -143,6 +152,15 @@ void OBSBasicSettings::LoadStream1Settings() idx = 0; } ui->server->setCurrentIndex(idx); + +#if CAFFEINE_ENABLED + if (username && username[0]) { + lastSignedInAs = username; + ui->authSignedInAsLabel->setVisible(true); + ui->authSignedInAs->setVisible(true); + ui->authSignedInAs->setText(QT_UTF8(username)); + } +#endif } ui->key->setText(key); @@ -186,6 +204,12 @@ void OBSBasicSettings::SaveStream1Settings() obs_data_set_string( settings, "server", QT_TO_UTF8(ui->server->currentData().toString())); +#if CAFFEINE_ENABLED + if (ui->service->currentText() == "Caffeine") { + obs_data_set_string(settings, "username", + QT_TO_UTF8(lastSignedInAs)); + } +#endif } else { obs_data_set_string(settings, "server", QT_TO_UTF8(ui->customServer->text())); @@ -349,11 +373,6 @@ void OBSBasicSettings::LoadServices(bool showAll) ui->service->blockSignals(false); } -static inline bool is_auth_service(const std::string &service) -{ - return Auth::AuthType(service) != Auth::Type::None; -} - void OBSBasicSettings::on_service_currentIndexChanged(int) { bool showMore = ui->service->currentData().toInt() == @@ -369,25 +388,54 @@ void OBSBasicSettings::on_service_currentIndexChanged(int) ui->twitchAddonDropdown->setVisible(false); ui->twitchAddonLabel->setVisible(false); -#ifdef BROWSER_AVAILABLE - if (cef) { - if (lastService != service.c_str()) { - QString key = ui->key->text(); - bool can_auth = is_auth_service(service); - int page = can_auth && (!loading || key.isEmpty()) - ? (int)Section::Connect - : (int)Section::StreamKey; - - ui->streamStackWidget->setCurrentIndex(page); - ui->streamKeyWidget->setVisible(true); - ui->streamKeyLabel->setVisible(true); - ui->connectAccount2->setVisible(can_auth); - } +#ifdef AUTH_ENABLED + auth.reset(); + + if (!!main->auth && + service.find(main->auth->service()) != std::string::npos) { + auth = main->auth; + OnAuthConnected(); + } + + bool can_auth = Auth::CanAuthService(service); + bool hidden_key = Auth::IsKeyHidden(service); + QString connectString = + QTStr("Basic.AutoConfig.StreamPage.ConnectAccount") + .arg(hidden_key ? QTStr("Required") + : QTStr("Optional")); + ui->connectAccount->setText(connectString); + ui->connectAccount2->setText(connectString); + + if (lastService.isEmpty()) { + lastService = service.c_str(); + lastServiceKey = ui->key->text(); + } else if (lastService != service.c_str()) { + // Don't show the stream key from the previous service + ui->key->clear(); } else { - ui->connectAccount2->setVisible(false); + ui->key->setText(lastServiceKey); } -#else - ui->connectAccount2->setVisible(false); + QString key = ui->key->text(); + bool authenticated = !key.isEmpty(); + int page = can_auth && (!loading || key.isEmpty()) + ? (int)Section::Connect + : (int)Section::StreamKey; + if (hidden_key) + page = (int)Section::StreamKey; + ui->useStreamKey->setVisible(!hidden_key); + ui->streamStackWidget->setCurrentIndex(page); + ui->streamKeyWidget->setVisible(!hidden_key); + ui->streamKeyLabel->setVisible(!hidden_key); +#if CAFFEINE_ENABLED + bool isCaffeine = service == "Caffeine"; + ui->authSignedInAs->setVisible(authenticated && isCaffeine); + ui->authSignedInAs->setText(lastSignedInAs); + ui->authSignedInAsLabel->setVisible(authenticated && isCaffeine); +#endif + ui->connectAccount->setVisible(can_auth && !authenticated); + ui->disconnectAccount->setVisible(can_auth && authenticated); + ui->connectAccount2->setVisible(can_auth && !authenticated); + #endif ui->useAuth->setVisible(custom); @@ -408,15 +456,7 @@ void OBSBasicSettings::on_service_currentIndexChanged(int) ui->serverStackedWidget->setCurrentIndex(0); } -#ifdef BROWSER_AVAILABLE - auth.reset(); - - if (!!main->auth && - service.find(main->auth->service()) != std::string::npos) { - auth = main->auth; - OnAuthConnected(); - } -#endif + update(); } void OBSBasicSettings::UpdateServerList() @@ -429,8 +469,6 @@ void OBSBasicSettings::UpdateServerList() LoadServices(true); ui->service->showPopup(); return; - } else { - lastService = serviceName; } obs_properties_t *props = obs_get_service_properties("rtmp_common"); @@ -507,7 +545,7 @@ OBSService OBSBasicSettings::SpawnTempService() void OBSBasicSettings::OnOAuthStreamKeyConnected() { -#ifdef BROWSER_AVAILABLE +#ifdef AUTH_ENABLED OAuthStreamKey *a = reinterpret_cast(auth.get()); if (a) { @@ -516,11 +554,24 @@ void OBSBasicSettings::OnOAuthStreamKeyConnected() if (validKey) ui->key->setText(QT_UTF8(a->key().c_str())); + lastService = a->service(); + lastServiceKey = a->key().c_str(); ui->streamKeyWidget->setVisible(false); ui->streamKeyLabel->setVisible(false); + ui->connectAccount->setVisible(false); ui->connectAccount2->setVisible(false); ui->disconnectAccount->setVisible(true); +#if CAFFEINE_ENABLED + if (std::string(a->service()) == "Caffeine") { + auto caffeine = dynamic_cast(a); + lastSignedInAs = caffeine->GetUsername().c_str(); + ui->authSignedInAsLabel->setVisible(true); + ui->authSignedInAs->setVisible(true); + ui->authSignedInAs->setText(lastSignedInAs); + } +#endif + if (strcmp(a->service(), "Twitch") == 0) { ui->bandwidthTestEnable->setVisible(true); ui->twitchAddonLabel->setVisible(true); @@ -531,6 +582,7 @@ void OBSBasicSettings::OnOAuthStreamKeyConnected() } ui->streamStackWidget->setCurrentIndex((int)Section::StreamKey); + update(); #endif } @@ -539,7 +591,7 @@ void OBSBasicSettings::OnAuthConnected() std::string service = QT_TO_UTF8(ui->service->currentText()); Auth::Type type = Auth::AuthType(service); - if (type == Auth::Type::OAuth_StreamKey) { + if (type != Auth::Type::None) { OnOAuthStreamKeyConnected(); } @@ -551,7 +603,7 @@ void OBSBasicSettings::OnAuthConnected() void OBSBasicSettings::on_connectAccount_clicked() { -#ifdef BROWSER_AVAILABLE +#ifdef AUTH_ENABLED std::string service = QT_TO_UTF8(ui->service->currentText()); OAuth::DeleteCookies(service); @@ -578,13 +630,18 @@ void OBSBasicSettings::on_disconnectAccount_clicked() return; } - main->auth.reset(); - auth.reset(); + // Remove the auth here if caffeine account is not associated + if (ui->service->currentText() != "Caffeine") { + main->auth.reset(); + auth.reset(); + } std::string service = QT_TO_UTF8(ui->service->currentText()); -#ifdef BROWSER_AVAILABLE + bool hidden_key = false; +#ifdef AUTH_ENABLED OAuth::DeleteCookies(service); + hidden_key = Auth::IsKeyHidden(service); #endif ui->bandwidthTestEnable->setChecked(false); @@ -592,11 +649,20 @@ void OBSBasicSettings::on_disconnectAccount_clicked() ui->streamKeyWidget->setVisible(true); ui->streamKeyLabel->setVisible(true); ui->connectAccount2->setVisible(true); + ui->connectAccount->setVisible(true); ui->disconnectAccount->setVisible(false); ui->bandwidthTestEnable->setVisible(false); + ui->authSignedInAs->setVisible(false); + ui->authSignedInAsLabel->setVisible(false); ui->twitchAddonDropdown->setVisible(false); ui->twitchAddonLabel->setVisible(false); ui->key->setText(""); + +// Hide stream keys for Caffeine we do not support it. +#if CAFFEINE_ENABLED + ui->streamKeyWidget->setVisible(false); + ui->streamKeyLabel->setVisible(false); +#endif } void OBSBasicSettings::on_useStreamKey_clicked() diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp index d0ee1a1d636950..f8d40727590602 100644 --- a/UI/window-basic-settings.cpp +++ b/UI/window-basic-settings.cpp @@ -3702,6 +3702,13 @@ bool OBSBasicSettings::QueryChanges() if (button == QMessageBox::Cancel) { return false; } else if (button == QMessageBox::Yes) { + // If Caffeine account was linked and disconnected reset the auth here. +#if CAFFEINE_ENABLED + if (ui->connectAccount2->isVisible()) { + main->auth.reset(); + auth.reset(); + } +#endif SaveSettings(); } else { if (savedTheme != App()->GetTheme()) @@ -3762,6 +3769,12 @@ void OBSBasicSettings::on_buttonBox_clicked(QAbstractButton *button) if (val == QDialogButtonBox::ApplyRole || val == QDialogButtonBox::AcceptRole) { +#if CAFFEINE_ENABLED + if (ui->connectAccount2->isVisible() && main->auth) { + main->auth.reset(); + auth.reset(); + } +#endif SaveSettings(); ClearChanged(); } diff --git a/UI/window-basic-settings.hpp b/UI/window-basic-settings.hpp index 4b5d352f20c42a..cff0ae54c87735 100644 --- a/UI/window-basic-settings.hpp +++ b/UI/window-basic-settings.hpp @@ -29,6 +29,7 @@ #include #include "auth-base.hpp" +#include "ui-config.h" class OBSBasic; class QAbstractButton; @@ -250,6 +251,10 @@ class OBSBasicSettings : public QDialog { QString lastService; int prevLangIndex; bool prevBrowserAccel; + QString lastServiceKey; +#if CAFFEINE_ENABLED + QString lastSignedInAs; +#endif private slots: void UpdateServerList(); void UpdateKeyLink(); diff --git a/UI/window-caffeine.cpp b/UI/window-caffeine.cpp new file mode 100644 index 00000000000000..4b025e007e2b76 --- /dev/null +++ b/UI/window-caffeine.cpp @@ -0,0 +1,151 @@ +#include "window-caffeine.hpp" +#include "window-basic-main.hpp" + +#include "ui_CaffeinePanel.h" + +#include +#include + +void CaffeineInfoPanel::registerDockWidget() +{ + this->setObjectName(QStringLiteral("caffeineDock")); + this->setWindowTitle(QTStr("Caffeine.Dock")); + this->setVisible(true); + OBSBasic::Get()->AddDockWidget(this); +} + +void CaffeineInfoPanel::updateClicked(bool) +{ + std::string title = std::string(ui->title->text().toUtf8().data()); + caff_Rating rating = ui->rating->itemData(ui->rating->currentIndex()) + .value(); + + setTitle(title); + setRating(rating); + + obs_service_t *service = OBSBasic::Get()->GetService(); + if (service) { + obs_data_t *data = obs_service_get_settings(service); + obs_data_set_string(data, "broadcast_title", + getTitle().c_str()); + obs_data_set_int(data, "rating", + static_cast(getRating())); + obs_service_update(service, data); + obs_data_release(data); + } +} + +void CaffeineInfoPanel::viewOnWebClicked(bool) +{ + QUrl url; + // Set the Caffeine URL default or staging + const char *host = getenv("LIBCAFFEINE_DOMAIN") == NULL + ? "caffeine.tv" + : "www.staging.caffeine.tv"; + url.setHost(host); + url.setPath(QString::fromStdString("/" + owner->GetUsername())); + url.setScheme("https"); + QDesktopServices::openUrl(url); +} + +CaffeineInfoPanel::CaffeineInfoPanel(CaffeineAuth *owner, + caff_InstanceHandle instance) + : OBSDock(OBSBasic::Get()), + owner(owner), + ui(new Ui::CaffeinePanel), + caffeineInstance(instance), + checkDroppedFramesTimer(this) +{ + ui->setupUi(this); + + // Set up ratings. + ui->rating->clear(); + ui->rating->addItem(QTStr("Caffeine.Rating.None"), + QVariant(caff_RatingNone)); + ui->rating->addItem(QTStr("Caffeine.Rating.SeventeenPlus"), + QVariant(caff_RatingSeventeenPlus)); + ui->rating->setCurrentIndex(static_cast(getRating())); + + // Set up title + ui->title->setText(QString::fromStdString(getTitle())); + ui->title->setAttribute(Qt::WA_MacShowFocusRect, false); + + // Buttons + connect(ui->updateButton, SIGNAL(clicked(bool)), + SLOT(updateClicked(bool))); + connect(ui->viewOnWebBtn, SIGNAL(clicked(bool)), + SLOT(viewOnWebClicked(bool))); + + // Set timer + checkDroppedFramesTimer.setInterval(500); + connect(&checkDroppedFramesTimer, SIGNAL(timeout()), this, + SLOT(checkDroppedFrames())); + checkDroppedFramesTimer.start(); + + // Set up warning popup message + showWarningMessageBox.setWindowTitle( + QTStr("Caffeine.SystemOverload.Title")); + showWarningMessageBox.setText(QTStr("Caffeine.SystemOverload.Text")); + showWarningMessageBox.setModal(false); + showWarningMessageBox.setIcon(QMessageBox::Warning); + showWarningMessageBox.addButton(QMessageBox::Ok); + checkBox = + new QCheckBox(QTStr("Caffeine.SystemOverload.CheckBox.Text")); + showWarningMessageBox.setCheckBox(checkBox); + + this->registerDockWidget(); +} + +CaffeineInfoPanel::~CaffeineInfoPanel() +{ + // Remove the Panel from OBS + checkDroppedFramesTimer.stop(); + OBSBasic::Get()->RemoveCaffeineDockWidget(this); +} + +std::string CaffeineInfoPanel::getTitle() +{ + OBSBasic *main = OBSBasic::Get(); + config_set_default_string(main->Config(), "Caffeine", "Title", + tr("Caffeine.Title").toUtf8().data()); + const char *title = + config_get_string(main->Config(), "Caffeine", "Title"); + if (!title) { + return std::string(tr("Caffeine.Title").toUtf8().data()); + } + return title; +} + +void CaffeineInfoPanel::setTitle(std::string title) +{ + OBSBasic *main = OBSBasic::Get(); + config_set_string(main->Config(), "Caffeine", "Title", title.c_str()); +} + +caff_Rating CaffeineInfoPanel::getRating() +{ + OBSBasic *main = OBSBasic::Get(); + config_set_default_int(main->Config(), "Caffeine", "Rating", + caff_RatingNone); + return static_cast( + config_get_int(main->Config(), "Caffeine", "Rating")); +} + +void CaffeineInfoPanel::setRating(caff_Rating rating) +{ + OBSBasic *main = OBSBasic::Get(); + config_set_int(main->Config(), "Caffeine", "Rating", + static_cast(rating)); +} + +void CaffeineInfoPanel::checkDroppedFrames() +{ + obs_service_t *service = OBSBasic::Get()->GetService(); + // Check OBS bool data variable - frames_dropped_above_threshold data variable + if (obs_data_get_bool(obs_service_get_settings(service), + "frames_dropped_above_threshold")) { + if (checkBox->checkState() == Qt::Unchecked) { + showWarningMessageBox.show(); + } + } +} diff --git a/UI/window-caffeine.hpp b/UI/window-caffeine.hpp new file mode 100644 index 00000000000000..7953e80e263e90 --- /dev/null +++ b/UI/window-caffeine.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include + +#include "window-dock.hpp" +#include "auth-caffeine.hpp" + +#include + +Q_DECLARE_METATYPE(caff_Rating); + +namespace Ui { +class CaffeinePanel; +} + +class CaffeineInfoPanel : public OBSDock { + Q_OBJECT + +private: + CaffeineAuth *owner; + std::shared_ptr ui; + caff_InstanceHandle caffeineInstance; + QTimer checkDroppedFramesTimer; + QMessageBox showWarningMessageBox; + QCheckBox *checkBox; + + void registerDockWidget(); + +public slots: + void updateClicked(bool); + + void viewOnWebClicked(bool); + void checkDroppedFrames(); + +public: + CaffeineInfoPanel(CaffeineAuth *owner, caff_InstanceHandle instance); + virtual ~CaffeineInfoPanel() override; + + std::string getTitle(); + void setTitle(std::string title); + + caff_Rating getRating(); + void setRating(caff_Rating rating); +}; diff --git a/bootstrap-osx.sh b/bootstrap-osx.sh new file mode 100755 index 00000000000000..437b1de8830ffe --- /dev/null +++ b/bootstrap-osx.sh @@ -0,0 +1,166 @@ +#!/usr/bin/env bash + +set -e + +VLC_VERSION=3.0.8 +SPARKLE_VERSION=1.23.0 +LIBCAFFEINE_VERSION=0.6.1 +QT_VERSION=5.14.1 +CEF_BUILD_VERSION=75.1.14+gc81164e+chromium-75.0.3770.100 +OBSDEPS_VERSION=2020-04-24 + +if ! which brew >> /dev/null +then + echo Installing homebrew + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +fi + +echo Installing dependencies from homebrew +brew update --preinstall +brew bundle --file ./CI/scripts/macos/Brewfile + + +echo Checking for VLC +VLC_PATH=$PWD/vlc-${VLC_VERSION} +if [ ! -d $VLC_PATH ] +then + [ -e ./vlc-${VLC_VERSION}.tar.xz ] || \ + curl -L -O https://downloads.videolan.org/vlc/${VLC_VERSION}/vlc-${VLC_VERSION}.tar.xz + echo Uncompressing VLC + tar xf vlc-${VLC_VERSION}.tar.xz + rm vlc-${VLC_VERSION}.tar.xz +fi + +echo Checking for Sparkle +if [ ! -d cmbuild/sparkle/Sparkle.framework ] +then + mkdir -p cmbuild/sparkle + [ -e ./sparkle.tar.bz2 ] || \ + curl -L -o sparkle.tar.bz2 https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-${SPARKLE_VERSION}.tar.bz2 + echo Uncompressing sparkle + tar xf ./sparkle.tar.bz2 -C cmbuild/sparkle + rm ./sparkle.tar.bz2 +fi + +[ -d /Library/Frameworks/Sparkle.framework ] || \ + sudo cp -R cmbuild/sparkle/Sparkle.framework /Library/Frameworks/Sparkle.framework + +echo Checking for OBS Project Dependencies +OBSDEPS_DIR=$PWD/cmbuild/obsdeps +if [ ! -d $OBSDEPS_DIR ] +then + pushd cmbuild + [ -e ./osx-deps-${OBSDEPS_VERSION}.tar.gz ] || \ + curl -L -O https://github.com/obsproject/obs-deps/releases/download/$OBSDEPS_VERSION/osx-deps-${OBSDEPS_VERSION}.tar.gz + echo Uncompressing OBS dependencies + tar xf ./osx-deps-${OBSDEPS_VERSION}.tar.gz + rm ./osx-deps-${OBSDEPS_VERSION}.tar.gz + popd +fi +[ -e /tmp/obsdeps ] || ln -s $OBSDEPS_DIR /tmp/obsdeps + +echo Checking for libcaffeine +LIBCAFFEINE_DIR=$PWD/libcaffeine-v${LIBCAFFEINE_VERSION}-macos +if [ ! -d $LIBCAFFEINE_DIR ] +then + [ -e ./libcaffeine-v${LIBCAFFEINE_VERSION}-macos.7z ] || \ + curl -L -O https://github.com/caffeinetv/libcaffeine/releases/download/v${LIBCAFFEINE_VERSION}/libcaffeine-v${LIBCAFFEINE_VERSION}-macos.7z + which 7z >> /dev/null || brew install p7zip + echo Uncompressing libcaffeine + 7z x libcaffeine-v${LIBCAFFEINE_VERSION}-macos.7z + rm libcaffeine-v${LIBCAFFEINE_VERSION}-macos.7z +fi + +CEF_BASENAME=cef_binary_${CEF_BUILD_VERSION}_macosx64 +CEF_BASENAME_MINIMAL=${CEF_BASENAME}_minimal + +if [ -z "${CEF_ROOT_DIR}" ] +then + CEF_ROOT_DIR=$PWD/cmbuild/${CEF_BASENAME_MINIMAL} + echo "export CEF_ROOT_DIR=$CEF_ROOT_DIR" >> $HOME/.zshrc +fi + +which cmake >> /dev/null || brew install cmake + +if [ ! -e ./cmbuild/${CEF_BASENAME} ] +then + if [ ! -e ${CEF_ROOT_DIR} ] + then + echo "Downloading and building CEF (this will take a while)" + pushd cmbuild + CEF_TARFILE=`echo ${CEF_BASENAME_MINIMAL}.tar.bz2 | sed 's/+/%2B/g'` + if [ ! -e ./${CEF_TARFILE} ] + then + echo Getting http://opensource.spotify.com/cefbuilds/$CEF_TARFILE + curl -L -O http://opensource.spotify.com/cefbuilds/$CEF_TARFILE + fi + echo "Uncompressing CEF" + tar xf ./${CEF_TARFILE} + rm ${CEF_TARFILE} + mkdir ${CEF_BASENAME_MINIMAL}/build + cd ${CEF_BASENAME_MINIMAL}/build + echo "Building CEF" + cmake -DCMAKE_CXX_FLAGS="-std=c++11 -stdlib=libc++" -DCMAKE_EXE_LINKER_FLAGS="-std=c++11 -stdlib=libc++" -DCMAKE_OSX_DEPLOYMENT_TARGET=10.11 .. + make -j + popd + fi + ln -s ${CEF_ROOT_DIR} ./cmbuild/${CEF_BASENAME} +fi + +echo Checking for QT +QT_DIR=/usr/local/Cellar/qt/$QT_VERSION +if [ ! -e $QT_DIR ] +then + pushd /tmp + curl -O https://gist.githubusercontent.com/DDRBoxman/9c7a2b08933166f4b61ed9a44b242609/raw/ef4de6c587c6bd7f50210eccd5bd51ff08e6de13/qt.rb + brew install ./qt.rb + popd +fi + +echo Checking for SWIG +if [ ! -e /usr/local/Cellar/swig/ ] +then + pushd /tmp + curl -O https://gist.githubusercontent.com/DDRBoxman/4cada55c51803a2f963fa40ce55c9d3e/raw/572c67e908bfbc1bcb8c476ea77ea3935133f5b5/swig.rb + brew install ./swig.rb + popd +fi + +echo Configuring and building +mkdir -p build +pushd build +cmake \ + -DENABLE_SPARKLE_UPDATER=ON \ + -DDISABLE_PYTHON=ON \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=10.11 \ + -DQTDIR="$QT_DIR" \ + -DDepsPath="$OBSDEPS_DIR" \ + -DVLCPath="$VLC_PATH" \ + -DENABLE_VLC=ON \ + -DBUILD_BROWSER=ON \ + -DBROWSER_DEPLOY=ON \ + -DBUILD_CAPTIONS=ON \ + -DWITH_RTMPS=ON \ + -DCEF_ROOT_DIR="$CEF_ROOT_DIR" \ + -DLIBCAFFEINE_DIR="$LIBCAFFEINE_DIR" \ + .. +make -j +popd + + +cat </obs-plugins/${_bit_suffix}") if(DEFINED ENV{obsInstallerTempDir}) - obs_debug_copy_helper(${target} "$ENV{obsInstallerTempDir}/${OBS_PLUGIN_DESTINATION}") + obs_debug_copy_helper(${target} "${obsInstallerTempDir}/${OBS_PLUGIN_DESTINATION}") endif() install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/pdbs/" @@ -369,7 +369,7 @@ function(install_obs_pdb ttype target) obs_debug_copy_helper(${target} "${OBS_OUTPUT_DIR}/$/bin/${_bit_suffix}") if(DEFINED ENV{obsInstallerTempDir}) - obs_debug_copy_helper(${target} "$ENV{obsInstallerTempDir}/${OBS_EXECUTABLE_DESTINATION}") + obs_debug_copy_helper(${target} "${obsInstallerTempDir}/${OBS_EXECUTABLE_DESTINATION}") endif() install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/pdbs/" @@ -412,7 +412,7 @@ function(install_obs_core target) add_custom_command(TARGET ${target} POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy "$" - "$ENV{obsInstallerTempDir}/${tmp_target_dir}/$" + "${obsInstallerTempDir}/${tmp_target_dir}/$" VERBATIM) endif() @@ -450,7 +450,7 @@ function(install_obs_bin target mode) add_custom_command(TARGET ${target} POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy "${bin}" - "$ENV{obsInstallerTempDir}/${OBS_EXECUTABLE_DESTINATION}/${fname}" + "${obsInstallerTempDir}/${OBS_EXECUTABLE_DESTINATION}/${fname}" VERBATIM) endif() endforeach() @@ -480,7 +480,7 @@ function(install_obs_plugin target) if(DEFINED ENV{obsInstallerTempDir}) add_custom_command(TARGET ${target} POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy - "$" "$ENV{obsInstallerTempDir}/${OBS_PLUGIN_DESTINATION}/$" + "$" "${obsInstallerTempDir}/${OBS_PLUGIN_DESTINATION}/$" VERBATIM) endif() @@ -499,7 +499,7 @@ function(install_obs_data target datadir datadest) if(CMAKE_SIZEOF_VOID_P EQUAL 8 AND DEFINED ENV{obsInstallerTempDir}) add_custom_command(TARGET ${target} POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy_directory - "${CMAKE_CURRENT_SOURCE_DIR}/${datadir}" "$ENV{obsInstallerTempDir}/${OBS_DATA_DESTINATION}/${datadest}" + "${CMAKE_CURRENT_SOURCE_DIR}/${datadir}" "${obsInstallerTempDir}/${OBS_DATA_DESTINATION}/${datadest}" VERBATIM) endif() endfunction() @@ -536,11 +536,11 @@ function(install_obs_data_file target datafile datadest) if(CMAKE_SIZEOF_VOID_P EQUAL 8 AND DEFINED ENV{obsInstallerTempDir}) add_custom_command(TARGET ${target} POST_BUILD COMMAND "${CMAKE_COMMAND}" -E make_directory - "$ENV{obsInstallerTempDir}/${OBS_DATA_DESTINATION}/${datadest}" + "${obsInstallerTempDir}/${OBS_DATA_DESTINATION}/${datadest}" VERBATIM) add_custom_command(TARGET ${target} POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy - "${CMAKE_CURRENT_SOURCE_DIR}/${datafile}" "$ENV{obsInstallerTempDir}/${OBS_DATA_DESTINATION}/${datadest}" + "${CMAKE_CURRENT_SOURCE_DIR}/${datafile}" "${obsInstallerTempDir}/${OBS_DATA_DESTINATION}/${datadest}" VERBATIM) endif() endfunction() @@ -559,7 +559,7 @@ function(install_obs_datatarget target datadest) add_custom_command(TARGET ${target} POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy "$" - "$ENV{obsInstallerTempDir}/${OBS_DATA_DESTINATION}/${datadest}/$" + "${obsInstallerTempDir}/${OBS_DATA_DESTINATION}/${datadest}/$" VERBATIM) endif() endfunction() diff --git a/formatcode.sh b/formatcode.sh index 9a0c1338be6b1f..f776cf229396cc 100755 --- a/formatcode.sh +++ b/formatcode.sh @@ -19,7 +19,9 @@ if [[ $OS = "Linux" || $OS = "Darwin" ]] ; then fi # Discover clang-format -if type clang-format-8 2> /dev/null ; then +if type clang-format-10 2> /dev/null ; then + CLANG_FORMAT=clang-format-10 +elif type clang-format-8 2> /dev/null ; then CLANG_FORMAT=clang-format-8 else CLANG_FORMAT=clang-format diff --git a/libobs/obs-output.h b/libobs/obs-output.h index 51093f49074502..65d1f4b2fafce7 100644 --- a/libobs/obs-output.h +++ b/libobs/obs-output.h @@ -28,6 +28,8 @@ extern "C" { #define OBS_OUTPUT_SERVICE (1 << 3) #define OBS_OUTPUT_MULTI_TRACK (1 << 4) #define OBS_OUTPUT_CAN_PAUSE (1 << 5) +#define OBS_OUTPUT_BANDWIDTH_TEST_DISABLED (1 << 6) +#define OBS_OUTPUT_HARDWARE_ENCODING_DISABLED (1 << 7) struct encoder_packet; diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 900bd8f31f9e04..0de5efca85c835 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -76,6 +76,10 @@ else() message(STATUS "obs-vst submodule not found! Please fetch/update submodules. obs-vst plugin disabled.") endif() +if(CAFFEINE_ENABLED) + add_subdirectory(caffeine) +endif() + add_subdirectory(image-source) add_subdirectory(obs-x264) add_subdirectory(obs-libfdk) diff --git a/plugins/caffeine/CMakeLists.txt b/plugins/caffeine/CMakeLists.txt new file mode 100644 index 00000000000000..f327137d83bd67 --- /dev/null +++ b/plugins/caffeine/CMakeLists.txt @@ -0,0 +1,47 @@ +project(caffeine) + +find_package(libcaffeine REQUIRED) +find_package(Threads REQUIRED) + +if(MSVC) + set(caffeine_PLATFORM_DEPS + shlwapi.lib + w32-pthreads) +endif() + +set(caffeine_HEADERS + caffeine-foreground-process.h + caffeine-settings.h + caffeine-sample-logger.h + caffeine-stopwatch.h + caffeine-tracked-frames.hpp) + +set(caffeine_SOURCES + caffeine-foreground-process.c + caffeine-module.c + caffeine-output.cpp + caffeine-tracked-frames.cpp + caffeine-sample-logger.c + caffeine-stopwatch.c) + +add_library(caffeine MODULE + ${caffeine_HEADERS} + ${caffeine_SOURCES}) + +target_link_libraries(caffeine + ${caffeine_PLATFORM_DEPS} + ${THREADS_LIBRARIES} + libobs + libcaffeine) + +install_obs_plugin_with_data(caffeine data) + +# TODO: There may be a better way to do this, but generator expression didn't +# work with the install_obs_bin helper +if(CMAKE_BUILD_TYPE STREQUAL "Debug") + set(copy_bin ${LIBCAFFEINE_BINARY_DEBUG}) +else() + set(copy_bin ${LIBCAFFEINE_BINARY_RELWITHDEBINFO}) +endif() + +install_obs_bin(caffeine "" ${copy_bin}) diff --git a/plugins/caffeine/README.md b/plugins/caffeine/README.md new file mode 100644 index 00000000000000..2f15affab5e059 --- /dev/null +++ b/plugins/caffeine/README.md @@ -0,0 +1,9 @@ +Caffeine Plugin +=============== + +Building +-------- + +1. Download a [pre-built](https://github.com/caffeinetv/libcaffeine/releases) version of libcaffeine or check out [libcaffeine](https://github.com/caffeinetv/libcaffeine) and build it. +2. Set the `LIBCAFFEINE_DIR` environment variable to the directory where you extracted the zip or cloned the repository +3. Run CMake for the OBS project. The caffeine plugin and UI components should be enabled diff --git a/plugins/caffeine/caffeine-foreground-process.c b/plugins/caffeine/caffeine-foreground-process.c new file mode 100644 index 00000000000000..372ae4b3b3f973 --- /dev/null +++ b/plugins/caffeine/caffeine-foreground-process.c @@ -0,0 +1,43 @@ +#include "caffeine-foreground-process.h" + +#include + +#ifndef _WIN32 +char *get_foreground_process_name() +{ + return NULL; +} +#else + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include + +char *get_foreground_process_name() +{ + HWND window = GetForegroundWindow(); + if (!window) + return NULL; + DWORD pid; + if (!GetWindowThreadProcessId(window, &pid)) + return NULL; + HANDLE process = + OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + if (!process) + return NULL; + + DWORD const buffer_size = 4096; + char *filename = NULL; + char *full_path = bzalloc(buffer_size); + if (GetProcessImageFileNameA(process, full_path, buffer_size)) { + PathRemoveExtensionA(full_path); + filename = bstrdup(PathFindFileNameA(full_path)); + } + + bfree(full_path); + CloseHandle(process); + return filename; +} + +#endif // _WIN32 diff --git a/plugins/caffeine/caffeine-foreground-process.h b/plugins/caffeine/caffeine-foreground-process.h new file mode 100644 index 00000000000000..f34d7f03a2ad07 --- /dev/null +++ b/plugins/caffeine/caffeine-foreground-process.h @@ -0,0 +1,11 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +char *get_foreground_process_name(); + +#ifdef __cplusplus +} +#endif diff --git a/plugins/caffeine/caffeine-module.c b/plugins/caffeine/caffeine-module.c new file mode 100644 index 00000000000000..07ab92dce088d4 --- /dev/null +++ b/plugins/caffeine/caffeine-module.c @@ -0,0 +1,47 @@ +#include +#include "caffeine.h" + +OBS_DECLARE_MODULE() +OBS_MODULE_USE_DEFAULT_LOCALE("caffeine", "en-US") + +MODULE_EXPORT char const *obs_module_description(void) +{ + return obs_module_text("CaffeineModule"); +} + +struct obs_output_info get_caffeine_output_info(); +struct obs_output_info caffeine_output_info; + +/* Converts libcaffeine log levels to OBS levels */ +static int caffeine_to_obs_log_level(caff_LogLevel level) +{ + switch (level) { + case caff_LogLevelAll: + case caff_LogLevelDebug: + return LOG_DEBUG; + case caff_LogLevelWarning: + return LOG_WARNING; + case caff_LogLevelError: + return LOG_ERROR; + case caff_LogLevelNone: + default: + /* Do not log */ + return 0; + } +} + +static void caffeine_log(caff_LogLevel level, char const *message) +{ + blog(caffeine_to_obs_log_level(level), "[libcaffeine] %s", message); +} + +bool obs_module_load(void) +{ + caffeine_output_info = get_caffeine_output_info(); + obs_register_output(&caffeine_output_info); + caff_Result result = caff_initialize("obs", OBS_VERSION, + caff_LogLevelDebug, caffeine_log); + return result == caff_ResultSuccess; +} + +void obs_module_unload(void) {} diff --git a/plugins/caffeine/caffeine-output.cpp b/plugins/caffeine/caffeine-output.cpp new file mode 100644 index 00000000000000..8561b77438fbd4 --- /dev/null +++ b/plugins/caffeine/caffeine-output.cpp @@ -0,0 +1,848 @@ + +#include + +#include +#include +#include +#include + +#include + +#include "caffeine-foreground-process.h" +#include "caffeine-settings.h" +#include "caffeine-stopwatch.h" +#include "caffeine-sample-logger.h" +#include "caffeine-tracked-frames.hpp" + +/* Uncomment this to log each call to raw_audio/video */ + +// #define TRACE_FRAMES + +/* Uncomment these lines and change path to use the sample log +This is a simple debug tool, so I didn't add code to auto-create directories, etc +So you'll need to make sure the directory path is available before use. +This causes a little bit of macro salsa, but we can remove it later */ + +// #define USE_SAMPLE_LOG +// #define VIDEO_SAMPLE_LOG_FILE ("C:\\Users\\Caffeine\\Desktop\\OBS_LOGS\\caffeine_raw_video_samples.csv") +// #define AUDIO_SAMPLE_LOG_FILE ("C:\\Users\\Caffeine\\Desktop\\OBS_LOGS\\caffeine_raw_audio_samples.csv") + +#define do_log(level, format, ...) \ + blog(level, "[caffeine output] " format, ##__VA_ARGS__) + +#define log_error(format, ...) do_log(LOG_ERROR, format, ##__VA_ARGS__) +#define log_warn(format, ...) do_log(LOG_WARNING, format, ##__VA_ARGS__) +#define log_info(format, ...) do_log(LOG_INFO, format, ##__VA_ARGS__) +#define log_debug(format, ...) do_log(LOG_DEBUG, format, ##__VA_ARGS__) + +#define trace() log_debug("%s", __func__) + +#define safe_delete(x) \ + do { \ + if (nullptr != x) { \ + delete x; \ + x = nullptr; \ + } \ + } while (0) + +#define set_error(output, fmt, ...) \ + do { \ + struct dstr message; \ + dstr_init(&message); \ + dstr_printf(&message, (fmt), ##__VA_ARGS__); \ + log_error("%s", message.array); \ + obs_output_set_last_error((output), message.array); \ + dstr_free(&message); \ + } while (false) + +#define caff_max(a, b) (((a) > (b)) ? (a) : (b)) +#define caff_min(a, b) (((a) < (b)) ? (a) : (b)) +#define caff_ms_to_ns(ms) (ms * 1000000ULL) + +#define CAFF_AUDIO_FORMAT AUDIO_FORMAT_16BIT +#define CAFF_AUDIO_FORMAT_TYPE int16_t +#define CAFF_AUDIO_LAYOUT SPEAKERS_STEREO +#define CAFF_AUDIO_LAYOUT_MUL 2 +#define CAFF_AUDIO_SAMPLERATE 48000ul +#define NANOSECONDS 1000000000ull + +static int const enforced_height = 1080; +static int const slow_connection_wait_ms = 66; +static uint64_t const av_sync_tolerance_window_ms = 105UL; + +struct caffeine_audio { + struct audio_data *frames; + struct caffeine_audio *next; +}; + +struct caffeine_output { + obs_output_t *output; + caff_InstanceHandle instance; + struct obs_video_info video_info; + uint64_t start_timestamp; + uint64_t timestamp_window_pos; + uint64_t timestamp_window_neg; + uint64_t video_last_timestamp; + uint64_t audio_last_timestamp; + size_t audio_planes; + size_t audio_size; + + volatile bool is_online; + pthread_t monitor_thread; + char *foreground_process; + char *game_id; + + pthread_t audio_thread; + struct caffeine_audio *audio_queue; + pthread_mutex_t audio_lock; + pthread_cond_t audio_cond; + bool audio_stop; + + bool is_slow_connection; + caffeine_stopwatch_t slow_connection_stopwatch; + + int test_frame_drop_percent; + + CaffeineFramesTracker *frames_tracker; + +#ifdef USE_SAMPLE_LOG + caffeine_stopwatch_t sample_stopwatch; + uint64_t raw_video_left_func_timestamp_ns; + uint64_t raw_audio_left_func_timestamp_ns; + caffeine_sample_logger_t raw_video_sample_logger; + caffeine_sample_logger_t raw_audio_sample_logger; +#endif +}; + +static void audio_data_copy(struct audio_data *left, struct audio_data *right) +{ + for (int idx = 0; idx < MAX_AV_PLANES; idx++) { + if (!left->data[idx]) { + right->data[idx] = NULL; + } else { + size_t size = left->frames * + sizeof(CAFF_AUDIO_FORMAT_TYPE) * + CAFF_AUDIO_LAYOUT_MUL; + right->data[idx] = + static_cast(bmalloc(size)); + memcpy(right->data[idx], left->data[idx], size); + } + } + right->frames = left->frames; + right->timestamp = left->timestamp; +} + +static void audio_data_free(struct audio_data *ptr) +{ + for (int idx = 0; idx < MAX_AV_PLANES; idx++) { + if (!ptr->data[idx]) + continue; + bfree(ptr->data[idx]); + } + bfree(ptr); +} + +static const char *caffeine_get_name(void *data) +{ + UNUSED_PARAMETER(data); + + return obs_module_text("CaffeineOutput"); +} + +static int caffeine_to_obs_error(caff_Result error) +{ + switch (error) { + case caff_ResultSuccess: + return OBS_OUTPUT_SUCCESS; + case caff_ResultOutOfCapacity: + case caff_ResultFailure: + case caff_ResultBroadcastFailed: + return OBS_OUTPUT_CONNECT_FAILED; + case caff_ResultInternetDisconnected: + return OBS_OUTPUT_DISCONNECTED; + case caff_ResultCaffeineUnreachable: + return OBS_OUTPUT_CONNECT_FAILED; + case caff_ResultTakeover: + default: + return OBS_OUTPUT_ERROR; + } +} + +caff_VideoFormat obs_to_caffeine_format(enum video_format format) +{ + switch (format) { + case VIDEO_FORMAT_I420: + return caff_VideoFormatI420; + case VIDEO_FORMAT_NV12: + return caff_VideoFormatNv12; + case VIDEO_FORMAT_YUY2: + return caff_VideoFormatYuy2; + case VIDEO_FORMAT_UYVY: + return caff_VideoFormatUyvy; + case VIDEO_FORMAT_BGRA: + return caff_VideoFormatBgra; + + case VIDEO_FORMAT_RGBA: + case VIDEO_FORMAT_I444: + case VIDEO_FORMAT_Y800: + case VIDEO_FORMAT_BGRX: + case VIDEO_FORMAT_YVYU: + default: + return caff_VideoFormatUnknown; + } +} + +static bool prepare_audio(struct caffeine_output *context, + const struct audio_data *frame, + struct audio_data *output) +{ + /* This fixes an issue where unencoded outputs have video & audio out of sync + * + * Copied/adapted from obs-outputs/flv-output + */ + + *output = *frame; + + if (frame->timestamp < context->start_timestamp) { + uint64_t duration = ((uint64_t)frame->frames) * NANOSECONDS / + CAFF_AUDIO_SAMPLERATE; + uint64_t end_ts = (frame->timestamp + duration); + uint64_t cutoff; + + if (end_ts <= context->start_timestamp) + return false; + + cutoff = context->start_timestamp - frame->timestamp; + output->timestamp += cutoff; + + cutoff = cutoff * CAFF_AUDIO_SAMPLERATE / NANOSECONDS; + + for (size_t i = 0; i < context->audio_planes; i++) + output->data[i] += + context->audio_size * (uint32_t)cutoff; + output->frames -= (uint32_t)cutoff; + } + + return true; +} + +static void *__cdecl caffeine_handle_audio(void *ptr) +{ + struct caffeine_output *context = (struct caffeine_output *)ptr; + + pthread_mutex_lock(&context->audio_lock); + while (!context->audio_stop) { + // Wait for a signal. + pthread_cond_wait(&context->audio_cond, &context->audio_lock); + if (context->audio_stop || !context->audio_queue) + continue; + + // Dequeue the front element. + while (context->audio_queue) { + // Grab the first element and unlock the mutex. + struct caffeine_audio *here = context->audio_queue; + context->audio_queue = here->next; + pthread_mutex_unlock(&context->audio_lock); + + // Send off the audio. + caff_sendAudio(context->instance, here->frames->data[0], + here->frames->frames); + + // Delete the dequeued element. + audio_data_free(here->frames); + bfree(here); + + // Lock the mutex again. + pthread_mutex_lock(&context->audio_lock); + } + } + pthread_mutex_unlock(&context->audio_lock); + + return NULL; +} + +static void *caffeine_create(obs_data_t *settings, obs_output_t *output) +{ + trace(); + UNUSED_PARAMETER(settings); + struct caffeine_output *context = + reinterpret_cast( + bzalloc(sizeof(struct caffeine_output))); + context->output = output; + + // Create mutex and condvar. + if (pthread_mutex_init(&context->audio_lock, NULL) != 0) { + goto fail; + } + if (pthread_cond_init(&context->audio_cond, NULL) != 0) { + goto fail; + } + + /* TODO: can we get this from the CaffeineAuth object somehow? */ + context->instance = caff_createInstance(); + if (!context->instance) { + goto fail; + } + + return context; +fail: + pthread_mutex_destroy(&context->audio_lock); + pthread_cond_destroy(&context->audio_cond); + caff_freeInstance(&context->instance); + bfree(context); + return NULL; +} + +static void caffeine_stream_started(void *data); +static void caffeine_stream_failed(void *data, caff_Result error); + +static bool caffeine_authenticate(struct caffeine_output *context) +{ + trace(); + + obs_output_t *output = context->output; + + obs_service_t *service = obs_output_get_service(output); + char const *refresh_token = obs_service_get_key(service); + + switch (caff_refreshAuth(context->instance, refresh_token)) { + case caff_ResultSuccess: + return true; + case caff_ResultInfoIncorrect: + set_error(output, "%s", obs_module_text("SigninFailed")); + return false; + case caff_ResultRefreshTokenRequired: + set_error(output, "%s", obs_module_text("ErrorMustSignIn")); + return false; + case caff_ResultFailure: + case caff_ResultAlreadyBroadcasting: + log_warn("%s", obs_module_text("WarningAlreadyStreaming")); + return false; + default: + set_error(output, "%s", obs_module_text("SigninFailed")); + return false; + } +} + +static bool caffeine_start(void *data) +{ + trace(); + struct caffeine_output *context = + reinterpret_cast(data); + obs_output_t *output = context->output; + + obs_output_set_media(output, obs_get_video(), obs_get_audio()); + + switch (caff_checkVersion()) { + case caff_ResultSuccess: + break; + case caff_ResultOldVersion: + set_error(output, "%s", obs_module_text("ErrorOldVersion")); + return false; + case caff_ResultFailure: + if (caff_checkInternetConnection() == + caff_ResultInternetDisconnected) { + set_error(output, "%s", + obs_module_text("ErrorInternetDisconnected")); + return false; + } + default: + log_warn("Failed to complete Caffeine version check"); + } + + if (!caffeine_authenticate(context)) + return false; + + context->is_slow_connection = false; + const char *caffeine_slow_connection = + getenv("CAFFEINE_SLOW_CONNECTION"); + if (NULL != caffeine_slow_connection) { + context->is_slow_connection = + (1 == + caff_min(caff_max(0, atoi(caffeine_slow_connection)), + 1)); + } + + context->test_frame_drop_percent = 0; + + const char *test_frame_drop_percent = + getenv("CAFFEINE_COBS_TEST_FRAME_DROP_PERCENT"); + if (NULL != test_frame_drop_percent) { + context->test_frame_drop_percent = caff_min( + caff_max(0, atoi(test_frame_drop_percent)), 99); + } + if (context->test_frame_drop_percent > 0) { + srand(caff_max(0, (os_gettime_ns() % INT32_MAX) - 1)); + } + + caffeine_stopwatch_init(&context->slow_connection_stopwatch); + if (context->is_slow_connection) { + caffeine_stopwatch_start(&context->slow_connection_stopwatch); + } + + context->start_timestamp = 0ULL; + safe_delete(context->frames_tracker); + context->frames_tracker = + new CaffeineFramesTracker(obs_service_get_settings( + obs_output_get_service(context->output))); + context->frames_tracker->caffeine_set_next_check_dropped_frames(0); + +#ifdef USE_SAMPLE_LOG + context->raw_video_left_func_timestamp_ns = 0UL; + context->raw_audio_left_func_timestamp_ns = 0UL; + caffeine_stopwatch_init(&context->sample_stopwatch); + caffeine_stopwatch_start(&context->sample_stopwatch); + caffeine_sample_logger_init(&context->raw_audio_sample_logger, + AUDIO_SAMPLE_LOG_FILE); + caffeine_sample_logger_init(&context->raw_video_sample_logger, + VIDEO_SAMPLE_LOG_FILE); +#endif + + if (!obs_get_video_info(&context->video_info)) { + set_error(output, "Failed to get video info"); + return false; + } + + if (context->video_info.output_height > enforced_height) + log_warn("For best video quality and reduced CPU usage," + " set output resolution to 720p or below"); + + caff_VideoFormat format = + obs_to_caffeine_format(context->video_info.output_format); + + if (format == caff_VideoFormatUnknown) { + set_error(output, "%s %s", obs_module_text("ErrorVideoFormat"), + get_video_format_name( + context->video_info.output_format)); + return false; + } + + struct audio_convert_info conversion = {}; + conversion.format = CAFF_AUDIO_FORMAT; + conversion.speakers = CAFF_AUDIO_LAYOUT; + conversion.samples_per_sec = CAFF_AUDIO_SAMPLERATE; + + obs_output_set_audio_conversion(output, &conversion); + + context->audio_planes = + get_audio_planes(conversion.format, conversion.speakers); + context->audio_size = + get_audio_size(conversion.format, conversion.speakers, 1); + + if (!obs_output_can_begin_data_capture(output, 0)) + return false; + + { // Initialize Audio + context->audio_stop = false; + context->audio_queue = NULL; + pthread_create(&context->audio_thread, NULL, + &caffeine_handle_audio, context); + } + + obs_service_t *service = obs_output_get_service(output); + obs_data_t *settings = obs_service_get_settings(service); + char const *title = obs_data_get_string(settings, BROADCAST_TITLE_KEY); + + caff_Rating rating = + (caff_Rating)obs_data_get_int(settings, BROADCAST_RATING_KEY); + obs_data_release(settings); + + caff_Result result = caff_startBroadcast(context->instance, context, + title, rating, NULL, + caffeine_stream_started, + caffeine_stream_failed); + + if (result) { + set_error(output, "%s", caff_resultString(result)); + return false; + } + return true; +} + +static void enumerate_games(void *data, char const *process_name, + char const *game_id, char const *game_name) +{ + struct caffeine_output *context = + reinterpret_cast(data); + if (strcmp(process_name, context->foreground_process) == 0) { + log_debug("Detected game [%s]: %s", game_id, game_name); + bfree(context->game_id); + context->game_id = bstrdup(game_id); + } +} + +static void *monitor_thread(void *data) +{ + trace(); + struct caffeine_output *context = + reinterpret_cast(data); + const uint64_t millisPerNano = 1000000; + const uint64_t stop_interval = 100 /*ms*/ * millisPerNano; + const uint64_t game_interval = 5000 /*ms*/ * millisPerNano; + const uint64_t service_interval = 1000 /*ms*/ * millisPerNano; + + uint64_t last_ts = os_gettime_ns(); + uint64_t cur_game_interval = 0; + uint64_t cur_service_interval = 0; + while (context->is_online) { + os_sleepto_ns(last_ts + stop_interval); + + // Update Timers + uint64_t now_ts = os_gettime_ns(); + int64_t delta_ts = now_ts - last_ts; + last_ts = now_ts; + cur_game_interval += delta_ts; + cur_service_interval += delta_ts; + + // Game Update + if (cur_game_interval > game_interval) { + cur_game_interval = 0; + context->foreground_process = + get_foreground_process_name(); + if (context->foreground_process) { + caff_enumerateGames(context->instance, context, + enumerate_games); + bfree(context->foreground_process); + context->foreground_process = NULL; + } + caff_setGameId(context->instance, context->game_id); + bfree(context->game_id); + context->game_id = NULL; + } + + // Service Update + if (cur_service_interval > service_interval) { + cur_service_interval = 0; + obs_service_t *service = + obs_output_get_service(context->output); + if (service) { + obs_data_t *data = + obs_service_get_settings(service); + caff_setTitle(context->instance, + obs_data_get_string( + data, + BROADCAST_TITLE_KEY)); + caff_setRating( + context->instance, + static_cast( + obs_data_get_int( + data, + BROADCAST_RATING_KEY))); + obs_data_release(data); + } + } + } + + return NULL; +} + +static void caffeine_stream_started(void *data) +{ + trace(); + struct caffeine_output *context = + reinterpret_cast(data); + context->is_online = true; + pthread_create(&context->monitor_thread, NULL, monitor_thread, context); + obs_output_begin_data_capture(context->output, 0); +} + +static void caffeine_stream_failed(void *data, caff_Result error) +{ + struct caffeine_output *context = + reinterpret_cast(data); + + if (!obs_output_get_last_error(context->output)) { + if (caff_checkInternetConnection() == + caff_ResultInternetDisconnected) { + set_error(context->output, "%s", + obs_module_text("ErrorInternetDisconnected")); + } else { + set_error(context->output, "%s: [%d] %s", + obs_module_text("ErrorStartStream"), error, + caff_resultString(error)); + } + } + + if (context->is_online) { + context->is_online = false; + pthread_join(context->monitor_thread, NULL); + } + + obs_output_signal_stop(context->output, caffeine_to_obs_error(error)); +} + +static void caffeine_raw_video(void *data, struct video_data *frame) +{ +#ifdef TRACE_FRAMES + trace(); +#endif + + struct caffeine_output *context = + reinterpret_cast(data); + +#ifdef USE_SAMPLE_LOG + uint64_t func_called_timestamp_ns = + caffeine_stopwatch_get_elapsed_ns(&context->sample_stopwatch); +#endif + + const char *reason_none = ""; + const char *reason_slow_connection = "connection throttle"; + const char *reason_av_desync = "av desync"; + const char *reason_test_drop_frame_percent = "test frame drop percent"; + const char *send_frame_reason = reason_none; + + bool send_frame = true; + if (context->is_slow_connection) { + uint64_t slow_connection_ms = caffeine_stopwatch_get_elapsed_ms( + &context->slow_connection_stopwatch); + if (slow_connection_ms < slow_connection_wait_ms) { + send_frame_reason = reason_slow_connection; + send_frame = false; + } else { + caffeine_stopwatch_reset( + &context->slow_connection_stopwatch); + } + } + + if (!context->start_timestamp) { + context->start_timestamp = frame->timestamp; + context->frames_tracker->caffeine_set_next_check_dropped_frames( + context->start_timestamp + caff_ms_to_ns(10000ULL)); + } + + uint64_t last_pair_timestamp_ns = context->audio_last_timestamp; + context->video_last_timestamp = frame->timestamp; + + bool should_stall = false; + uint64_t timestamp_window_pos = context->timestamp_window_pos; + uint64_t timestamp_window_neg = context->timestamp_window_neg; + if ((timestamp_window_pos > 0) && (timestamp_window_neg > 0)) { + if (frame->timestamp > context->timestamp_window_pos) { + send_frame_reason = reason_av_desync; + send_frame = false; + should_stall = true; + } else if (frame->timestamp < context->timestamp_window_neg) { + send_frame_reason = reason_av_desync; + send_frame = false; + } + } + + if (send_frame && (context->test_frame_drop_percent > 0)) { + send_frame = (rand() % 100) > context->test_frame_drop_percent; + if (!send_frame) { + send_frame_reason = reason_test_drop_frame_percent; + } + } + + uint32_t width = context->video_info.output_width; + uint32_t height = context->video_info.output_height; + size_t total_bytes = (size_t)frame->linesize[0] * (size_t)height; + caff_VideoFormat format = + obs_to_caffeine_format(context->video_info.output_format); + int64_t timestampMicros = frame->timestamp / 1000; + if (send_frame) { + caff_sendVideo(context->instance, format, frame->data[0], + total_bytes, width, height, timestampMicros); + } + + if (context->frames_tracker != nullptr) { + context->frames_tracker->caffeine_add_frame(frame->timestamp, + send_frame); + } + +#ifdef USE_SAMPLE_LOG + uint64_t func_complete_timestamp_ns = + caffeine_stopwatch_get_elapsed_ns(&context->sample_stopwatch); + uint64_t func_time_ns = + func_complete_timestamp_ns - func_called_timestamp_ns; + uint64_t obs_app_time_ns = func_called_timestamp_ns - + context->raw_video_left_func_timestamp_ns; + caffeine_sample_logger_log_sample( + &context->raw_video_sample_logger, send_frame, + send_frame_reason, func_called_timestamp_ns, frame->timestamp, + last_pair_timestamp_ns, func_time_ns, obs_app_time_ns); + context->raw_video_left_func_timestamp_ns = + caffeine_stopwatch_get_elapsed_ns(&context->sample_stopwatch); +#endif + + if (should_stall) { // +105ms ahead. let's stall for 84ms since audio samples are coming in at 21.333 average + os_sleep_ms(84); + } +} + +static void caffeine_raw_audio(void *data, struct audio_data *frames) +{ +#ifdef TRACE_FRAMES + trace(); +#endif + struct caffeine_output *context = + reinterpret_cast(data); + +#ifdef USE_SAMPLE_LOG + uint64_t func_called_timestamp_ns = + caffeine_stopwatch_get_elapsed_ns(&context->sample_stopwatch); +#endif + + // Ensure that everything is initialized and still available. + if (context->audio_stop) + return; + + // Ensure that we are actually live and have started streaming. + if (!context->start_timestamp) + return; + + // Cut off or abort audio data if it does not start at our intended time. + struct audio_data in; + if (!prepare_audio(context, frames, &in)) { + return; + } + + uint64_t last_pair_timestamp_ns = context->video_last_timestamp; + + uint64_t duration = ((uint64_t)frames->frames) * NANOSECONDS / + CAFF_AUDIO_SAMPLERATE; + uint64_t end_ts = (frames->timestamp + duration); + uint64_t center_samples_ts = (frames->timestamp + end_ts) / 2; + context->audio_last_timestamp = center_samples_ts; + + const uint64_t timestamp_adj = + caff_ms_to_ns(av_sync_tolerance_window_ms); + context->timestamp_window_pos = center_samples_ts + timestamp_adj; + context->timestamp_window_neg = center_samples_ts - timestamp_adj; + + // Create a copy of the audio data for queuing. + // ToDo: Can this be optimized to use circlebuf for data? + struct caffeine_audio *ca = + (struct caffeine_audio *)bmalloc(sizeof(struct caffeine_audio)); + ca->next = NULL; + ca->frames = (struct audio_data *)bmalloc(sizeof(struct audio_data)); + audio_data_copy(&in, ca->frames); + + // Enqueue Audio + pthread_mutex_lock(&context->audio_lock); + { + // Find the last element that we can write to. This looks a bit weird, + // but it is a pointer to the last 'next' entry that is valid. + struct caffeine_audio **tgt = &context->audio_queue; + while ((*tgt) != NULL) + tgt = &((*tgt)->next); + *tgt = ca; + } + pthread_cond_signal(&context->audio_cond); + pthread_mutex_unlock(&context->audio_lock); + +#ifdef USE_SAMPLE_LOG + uint64_t func_complete_timestamp_ns = + caffeine_stopwatch_get_elapsed_ns(&context->sample_stopwatch); + uint64_t func_time_ns = + func_complete_timestamp_ns - func_called_timestamp_ns; + uint64_t obs_app_time_ns = func_called_timestamp_ns - + context->raw_audio_left_func_timestamp_ns; + caffeine_sample_logger_log_sample(&context->raw_audio_sample_logger, + true, "", func_called_timestamp_ns, + center_samples_ts, + last_pair_timestamp_ns, func_time_ns, + obs_app_time_ns); + context->raw_audio_left_func_timestamp_ns = + caffeine_stopwatch_get_elapsed_ns(&context->sample_stopwatch); +#endif +} + +static void caffeine_stop(void *data, uint64_t ts) +{ + trace(); + /* TODO: do something with this? */ + UNUSED_PARAMETER(ts); + + struct caffeine_output *context = + reinterpret_cast(data); + obs_output_t *output = context->output; + + if (context->is_online) { + context->is_online = false; + pthread_join(context->monitor_thread, NULL); + } + + { // Clean up Audio + { // Signal thread to stop and join with it. + pthread_mutex_lock(&context->audio_lock); + context->audio_stop = true; + pthread_cond_signal(&context->audio_cond); + pthread_mutex_unlock(&context->audio_lock); + pthread_join(context->audio_thread, NULL); + } + while (context->audio_queue) { + // clean up any remaining data. + struct caffeine_audio *here = context->audio_queue; + context->audio_queue = here->next; + audio_data_free(here->frames); + bfree(here); + } + } + + caff_endBroadcast(context->instance); + obs_output_end_data_capture(output); + + obs_data_set_bool(obs_service_get_settings( + obs_output_get_service(context->output)), + "frames_dropped_above_threshold", false); + + context->start_timestamp = 0ULL; + safe_delete(context->frames_tracker); +} + +static void caffeine_destroy(void *data) +{ + trace(); + struct caffeine_output *context = + reinterpret_cast(data); + caff_freeInstance(&context->instance); + + // Free mutex and condvar. + pthread_mutex_destroy(&context->audio_lock); + pthread_cond_destroy(&context->audio_cond); + + bfree(data); +} + +static float caffeine_get_congestion(void *data) +{ + struct caffeine_output *context = + reinterpret_cast(data); + + caff_ConnectionQuality quality = + caff_getConnectionQuality(context->instance); + + switch (quality) { + case caff_ConnectionQualityGood: + return 0.f; + case caff_ConnectionQualityPoor: + return 1.f; + default: + return 0.5f; + } +} + +extern "C" { +struct obs_output_info get_caffeine_output_info() +{ + struct obs_output_info output_info = {}; + memset(&output_info, 0, sizeof(obs_output_info)); + output_info.id = "caffeine_output"; + output_info.flags = OBS_OUTPUT_AV | OBS_OUTPUT_SERVICE | + OBS_OUTPUT_BANDWIDTH_TEST_DISABLED | + OBS_OUTPUT_HARDWARE_ENCODING_DISABLED; + + output_info.get_name = caffeine_get_name; + + output_info.create = caffeine_create; + output_info.destroy = caffeine_destroy; + + output_info.start = caffeine_start; + output_info.stop = caffeine_stop; + + output_info.raw_video = caffeine_raw_video; + output_info.raw_audio = caffeine_raw_audio; + return output_info; +} +} diff --git a/plugins/caffeine/caffeine-sample-logger.c b/plugins/caffeine/caffeine-sample-logger.c new file mode 100644 index 00000000000000..45f2888489db71 --- /dev/null +++ b/plugins/caffeine/caffeine-sample-logger.c @@ -0,0 +1,104 @@ +#include "caffeine-sample-logger.h" +#include +#include + +void caffeine_sample_logger_log(caffeine_sample_logger_t *lpsl, + const char *format, ...) +{ + if (false == lpsl->is_ok) { + return; + } + va_list args; + memset(lpsl->line_buff, 0, sizeof(lpsl->line_buff)); + va_start(args, format); + int num_chars = vsnprintf(lpsl->line_buff, sizeof(lpsl->line_buff), + format, args); + va_end(args); + FILE *file_handle = fopen(lpsl->file_name, "a+b"); + if (file_handle) { + fwrite(lpsl->line_buff, 1, num_chars, file_handle); + fclose(file_handle); + } +} + +bool caffeine_sample_logger_init(caffeine_sample_logger_t *lpsl, + const char *file_name) +{ + memset(lpsl, 0, sizeof(caffeine_sample_logger_t)); + strncpy(lpsl->file_name, file_name, sizeof(lpsl->file_name)); + FILE *file_handle = fopen(lpsl->file_name, "w+b"); + if (file_handle) { + fclose(file_handle); + lpsl->is_ok = true; + } + caffeine_sample_logger_log(lpsl, + "sample" + ",sent to libcaffeine" + ",reason" + ",wall time (ms)" + ",wall time delta (ms)" + ",sample obs timestamp (ms)" + ",sample obs timestamp delta (ms)" + ",sample obs timestamp delta from pair (ms)" + ",caffeine func time (ms)" + ",caffeine func time delta (ms)" + ",obs app time (ms)" + ",obs app time delta (ms)" + "\r\n"); + return lpsl->is_ok; +} + +void caffeine_sample_logger_log_sample( + caffeine_sample_logger_t *lpsl, bool sample_sent_to_libcaffeine, + const char *reason_not_sent_to_libcaffeine, uint64_t wall_time_ns, + uint64_t sample_obs_timestamp_ns, uint64_t pair_obs_last_timestamp_ns, + uint64_t caffeine_func_time_ns, uint64_t obs_app_time_ns) +{ + double wall_time_ms = (double)wall_time_ns / 1000000.0; + double sample_obs_timestamp_ms = + (double)sample_obs_timestamp_ns / 1000000.0; + double caffeine_func_time_ms = + (double)caffeine_func_time_ns / 1000000.0; + double obs_app_time_ms = (double)obs_app_time_ns / 1000000.0; + const char *sample_sent_yn = (true == sample_sent_to_libcaffeine) ? "Y" + : "N"; + double pair_obs_last_timestamp_ms = + (double)pair_obs_last_timestamp_ns / 1000000.0; + double pair_obs_timestamp_delta_from_pair_ms = + sample_obs_timestamp_ms - pair_obs_last_timestamp_ms; + + if (lpsl->sample_cnt == 0) { + caffeine_sample_logger_log( + lpsl, "%d,%s,%s,%0.3f,0,%0.3f,0,0,%0.3f,0,%0.3f,0\r\n", + lpsl->sample_cnt, sample_sent_yn, + reason_not_sent_to_libcaffeine, wall_time_ms, + sample_obs_timestamp_ms, caffeine_func_time_ms, + obs_app_time_ms); + } else { + double wall_time_ms_delta = + wall_time_ms - lpsl->prev_wall_time_ms; + double sample_obs_timestamp_ms_delta = + sample_obs_timestamp_ms - + lpsl->prev_sample_obs_timestamp_ms; + double caffeine_func_time_ms_delta = + caffeine_func_time_ms - + lpsl->prev_caffeine_func_time_ms; + double obs_app_time_ms_delta = + obs_app_time_ms - lpsl->prev_obs_app_time_ms; + caffeine_sample_logger_log( + lpsl, + "%d,%s,%s,%0.3f,%0.3f,%0.3f,%0.3f,%0.3f,%0.3f,%0.3f,%0.3f,%0.3f\r\n", + lpsl->sample_cnt, sample_sent_yn, + reason_not_sent_to_libcaffeine, wall_time_ms, + wall_time_ms_delta, sample_obs_timestamp_ms, + sample_obs_timestamp_ms_delta, + pair_obs_timestamp_delta_from_pair_ms, + caffeine_func_time_ms, caffeine_func_time_ms_delta, + obs_app_time_ms, obs_app_time_ms_delta); + } + lpsl->prev_wall_time_ms = wall_time_ms; + lpsl->prev_sample_obs_timestamp_ms = sample_obs_timestamp_ms; + lpsl->prev_caffeine_func_time_ms = caffeine_func_time_ms; + lpsl->prev_obs_app_time_ms = obs_app_time_ms; + lpsl->sample_cnt++; +} diff --git a/plugins/caffeine/caffeine-sample-logger.h b/plugins/caffeine/caffeine-sample-logger.h new file mode 100644 index 00000000000000..a1b138c4e7ad41 --- /dev/null +++ b/plugins/caffeine/caffeine-sample-logger.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct caffeine_sample_logger { + char file_name[1000]; + char line_buff[1000]; + bool is_ok; + int sample_cnt; + double prev_wall_time_ms; + double prev_sample_obs_timestamp_ms; + double prev_caffeine_func_time_ms; + double prev_obs_app_time_ms; +} caffeine_sample_logger_t; + +bool caffeine_sample_logger_init(caffeine_sample_logger_t *lpsl, + const char *file_name); + +void caffeine_sample_logger_log_sample( + caffeine_sample_logger_t *lpsl, bool sample_sent_to_libcaffeine, + const char *reason_not_sent_to_libcaffeine, uint64_t wall_time_ns, + uint64_t sample_obs_timestamp_ns, uint64_t pair_obs_last_timestamp_ns, + uint64_t caffeine_func_time_ns, uint64_t obs_app_time_ns); + +#ifdef __cplusplus +} +#endif diff --git a/plugins/caffeine/caffeine-settings.h b/plugins/caffeine/caffeine-settings.h new file mode 100644 index 00000000000000..edc32c8071f460 --- /dev/null +++ b/plugins/caffeine/caffeine-settings.h @@ -0,0 +1,4 @@ +#pragma once + +#define BROADCAST_RATING_KEY "rating" +#define BROADCAST_TITLE_KEY "broadcast_title" diff --git a/plugins/caffeine/caffeine-stopwatch.c b/plugins/caffeine/caffeine-stopwatch.c new file mode 100644 index 00000000000000..10200107727e2b --- /dev/null +++ b/plugins/caffeine/caffeine-stopwatch.c @@ -0,0 +1,66 @@ +#include "caffeine-stopwatch.h" +#include +#include + +void caffeine_stopwatch_init(caffeine_stopwatch_t *lpsw) +{ + memset(lpsw, 0, sizeof(caffeine_stopwatch_t)); +} + +void caffeine_stopwatch_start(caffeine_stopwatch_t *lpsw) +{ + uint64_t time_ns = 0UL; + if (!lpsw->running) { + time_ns = os_gettime_ns(); + lpsw->start_timestamp = time_ns; + lpsw->value_timestamp = time_ns; + lpsw->running = true; + } +} + +void caffeine_stopwatch_stop(caffeine_stopwatch_t *lpsw) +{ + uint64_t time_ns = 0UL; + if (!lpsw->running) { + time_ns = os_gettime_ns(); + lpsw->accumulator += + (lpsw->value_timestamp - lpsw->start_timestamp); + lpsw->start_timestamp = 0L; + lpsw->value_timestamp = 0L; + lpsw->running = false; + } +} + +void caffeine_stopwatch_reset(caffeine_stopwatch_t *lpsw) +{ + bool is_running = lpsw->running; + caffeine_stopwatch_init(lpsw); + if (is_running) { + caffeine_stopwatch_start(lpsw); + } +} + +uint64_t caffeine_stopwatch_get_elapsed_ns(caffeine_stopwatch_t *lpsw) +{ + uint64_t time_ns = 0UL; + uint64_t accumulation_total = lpsw->accumulator; + if (lpsw->running) { + time_ns = os_gettime_ns(); + lpsw->value_timestamp = time_ns; + accumulation_total += + lpsw->value_timestamp - lpsw->start_timestamp; + } + return accumulation_total; +} + +uint64_t caffeine_stopwatch_get_elapsed_ms(caffeine_stopwatch_t *lpsw) +{ + uint64_t elapsed_ns = caffeine_stopwatch_get_elapsed_ns(lpsw); + return elapsed_ns / 1000000UL; +} + +void caffeine_stopwatch_copy_state(caffeine_stopwatch_t *lpsw_dest, + caffeine_stopwatch_t *lpsw_src) +{ + memcpy(lpsw_dest, lpsw_src, sizeof(caffeine_stopwatch_t)); +} diff --git a/plugins/caffeine/caffeine-stopwatch.h b/plugins/caffeine/caffeine-stopwatch.h new file mode 100644 index 00000000000000..3632d6f3d12847 --- /dev/null +++ b/plugins/caffeine/caffeine-stopwatch.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct caffeine_stopwatch { + bool running; + uint64_t accumulator; + uint64_t start_timestamp; + uint64_t value_timestamp; +} caffeine_stopwatch_t; + +void caffeine_stopwatch_init(caffeine_stopwatch_t *lpsw); +void caffeine_stopwatch_start(caffeine_stopwatch_t *lpsw); +void caffeine_stopwatch_stop(caffeine_stopwatch_t *lpsw); +void caffeine_stopwatch_reset(caffeine_stopwatch_t *lpsw); +uint64_t caffeine_stopwatch_get_elapsed_ns(caffeine_stopwatch_t *lpsw); +uint64_t caffeine_stopwatch_get_elapsed_ms(caffeine_stopwatch_t *lpsw); +void caffeine_stopwatch_copy_state(caffeine_stopwatch_t *lpsw_dest, + caffeine_stopwatch_t *lpsw_src); + +#ifdef __cplusplus +} +#endif diff --git a/plugins/caffeine/caffeine-tracked-frames.cpp b/plugins/caffeine/caffeine-tracked-frames.cpp new file mode 100644 index 00000000000000..7f85cdcce17425 --- /dev/null +++ b/plugins/caffeine/caffeine-tracked-frames.cpp @@ -0,0 +1,66 @@ +#include "caffeine-tracked-frames.hpp" +#include + +#define caff_ms_to_ns(ms) (ms * 1000000ULL) +static uint32_t const threshold_frames_dropped_percent = 25; + +CaffeineFramesTracker::CaffeineFramesTracker(obs_data_t *data) + : frames_list(new std::list()), + next_check_dropped_frames(0), + data(data) +{ +} + +CaffeineFramesTracker::~CaffeineFramesTracker() +{ + delete frames_list; +} + +void CaffeineFramesTracker::caffeine_add_frame(uint64_t timestamp, bool sent) +{ + frames_list->push_back({timestamp, sent}); + obs_data_set_bool(data, "frames_dropped_above_threshold", false); + if (timestamp >= next_check_dropped_frames) { + caffeine_remove_old_frames(timestamp); + if (caffeine_get_drop_percent() >= + threshold_frames_dropped_percent) { + obs_data_set_bool( + data, "frames_dropped_above_threshold", true); + } else { + obs_data_set_bool( + data, "frames_dropped_above_threshold", false); + } + // Next check in 1 second + next_check_dropped_frames = timestamp + caff_ms_to_ns(1000ULL); + } +} + +void CaffeineFramesTracker::caffeine_remove_old_frames(uint64_t timestamp) +{ + uint64_t frame_time_lower_bound = timestamp - caff_ms_to_ns(10000ULL); + // remove old frames , Lower bound is 10 seconds + frames_list->remove_if( + [frame_time_lower_bound]( + const caffeine_tracked_frame &list_frame) -> bool { + return list_frame.timestamp < frame_time_lower_bound; + }); +} + +uint32_t CaffeineFramesTracker::caffeine_get_drop_percent() +{ + float frames_dropped = 0.0f; + float total_frames = 0.0f; + for (auto f = frames_list->begin(); f != frames_list->end(); f++) { + if (!f->sent) { + frames_dropped = frames_dropped + 1.0f; + } + total_frames = total_frames + 1.0f; + } + return (uint32_t)((frames_dropped / total_frames) * 100.0f); +} + +void CaffeineFramesTracker::caffeine_set_next_check_dropped_frames( + uint64_t next_check) +{ + next_check_dropped_frames = next_check; +} diff --git a/plugins/caffeine/caffeine-tracked-frames.hpp b/plugins/caffeine/caffeine-tracked-frames.hpp new file mode 100644 index 00000000000000..566ce3b39df917 --- /dev/null +++ b/plugins/caffeine/caffeine-tracked-frames.hpp @@ -0,0 +1,26 @@ +#ifndef CAFFEINE_TRACKED_FRAMES_H +#define CAFFEINE_TRACKED_FRAMES_H + +#include +#include + +struct caffeine_tracked_frame { + uint64_t timestamp; + bool sent; +}; + +class CaffeineFramesTracker { +public: + CaffeineFramesTracker(obs_data_t *data); + ~CaffeineFramesTracker(); + void caffeine_add_frame(uint64_t timestamp, bool sent); + void caffeine_set_next_check_dropped_frames(uint64_t next_check); + +private: + std::list *frames_list; + uint64_t next_check_dropped_frames; + void caffeine_remove_old_frames(uint64_t timestamp); + uint32_t caffeine_get_drop_percent(); + obs_data_t *data; +}; +#endif \ No newline at end of file diff --git a/plugins/caffeine/data/locale/en-US.ini b/plugins/caffeine/data/locale/en-US.ini new file mode 100644 index 00000000000000..7326df79f0c3de --- /dev/null +++ b/plugins/caffeine/data/locale/en-US.ini @@ -0,0 +1,10 @@ +CaffeineOutput="Caffeine.tv" +CaffeineModule="Caffeine.tv output" +SigninFailed="There was an error signing in." +ErrorMustSignIn="Go to Settings > Stream to sign into Caffeine." +ErrorOldVersion="Update to the latest version of OBS with Caffeine to broadcast." +ErrorAspectRatio="Caffeine requires using an output resolution of 1280x720. Go to Settings > Video to change the resolution." +ErrorVideoFormat="Caffeine requires using NV12 or I420 formats. Go to Settings > Advanced to change the Color Format." +ErrorStartStream="Failed to start stream. Try again." +WarningAlreadyStreaming="Caffeine is already broadcasting" +ErrorInternetDisconnected="Internet Connection Lost.\nYou have lost your connection. Please reconnect" diff --git a/plugins/obs-qsv11/QSV_Encoder_Internal.cpp b/plugins/obs-qsv11/QSV_Encoder_Internal.cpp index 4de63b43e32eab..757975dc845542 100644 --- a/plugins/obs-qsv11/QSV_Encoder_Internal.cpp +++ b/plugins/obs-qsv11/QSV_Encoder_Internal.cpp @@ -734,4 +734,4 @@ mfxStatus QSV_Encoder_Internal::Reset(qsv_param_t *pParams) MSDK_CHECK_RESULT(sts, MFX_ERR_NONE, sts); return sts; -} +} \ No newline at end of file diff --git a/plugins/obs-qsv11/obs-qsv11-plugin-main.c b/plugins/obs-qsv11/obs-qsv11-plugin-main.c index c22d77038bfcef..8fa1f85d3c344f 100644 --- a/plugins/obs-qsv11/obs-qsv11-plugin-main.c +++ b/plugins/obs-qsv11/obs-qsv11-plugin-main.c @@ -64,7 +64,6 @@ MODULE_EXPORT const char *obs_module_description(void) } extern struct obs_encoder_info obs_qsv_encoder; -extern struct obs_encoder_info obs_qsv_encoder_tex; bool obs_module_load(void) { @@ -77,7 +76,6 @@ bool obs_module_load(void) if (sts == MFX_ERR_NONE) { obs_register_encoder(&obs_qsv_encoder); - obs_register_encoder(&obs_qsv_encoder_tex); MFXClose(session); } else { impl = MFX_IMPL_HARDWARE_ANY | MFX_IMPL_VIA_D3D9; diff --git a/plugins/rtmp-services/data/services.json b/plugins/rtmp-services/data/services.json index dadf71091ab8fa..c96463099ea2f9 100644 --- a/plugins/rtmp-services/data/services.json +++ b/plugins/rtmp-services/data/services.json @@ -1878,6 +1878,23 @@ "max audio bitrate": 192, "x264opts": "tune=zerolatency scenecut=0" } + }, + { + "name": "Caffeine", + "common": true, + "servers": [ + { + "name": "Primary", + "url": "http://www.caffeine.tv" + } + ], + "recommended": { + "keyint": 3, + "output": "caffeine_output", + "profile": "main", + "max video bitrate": 2000, + "max audio bitrate": 128 + } } ] } diff --git a/plugins/rtmp-services/rtmp-common.c b/plugins/rtmp-services/rtmp-common.c index 2284a6c541f8d8..1d930d6b49a05d 100644 --- a/plugins/rtmp-services/rtmp-common.c +++ b/plugins/rtmp-services/rtmp-common.c @@ -315,11 +315,15 @@ static json_t *open_services_file(void) char *file; json_t *root = NULL; + // Don't use the file from user config. + // Temporary fix until we land caffeine service in OBS mainline. + /* file = obs_module_config_path("services.json"); if (file) { root = open_json_file(file); bfree(file); } + */ if (!root) { file = obs_module_file("services.json");