├── .github └── workflows │ ├── ci.yml │ └── docs.yml ├── .gitignore ├── .idea └── icon.svg ├── .kotlin └── errors │ ├── errors-1721380185468.log │ ├── errors-1724916413735.log │ ├── errors-1725028164833.log │ ├── errors-1725898521690.log │ └── errors-1727005936545.log ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── misc ├── TwoStagedTrackSelection │ ├── FirstStage_constrainttrackselector.pdf │ └── TwoStagedStreamSelection_overview.pdf ├── icon.svg ├── icon_old.svg ├── logo_shadow.png ├── newplayer_architecture.svg ├── screenshots │ ├── 373685724-42609e51-6bf7-4008-b084-a59ce111f3c1.png │ ├── 373686583-1164cf7c-66eb-48be-aeda-55e6e6294cf1.png │ ├── 373688583-9011749c-3aec-4bf7-a368-40000c84f8e3.png │ ├── 373689058-9fc27dfd-7f89-48de-b0ff-cd6e9bd4fdbd.png │ ├── 373690788-a7af6db0-eac2-4913-8f60-3d621c5afd9f.png │ ├── 373690876-9b96ae22-d537-4b49-ac1c-4549a94bebcb.png │ ├── 373691456-4aaff87d-dbf8-4877-866b-60e6fc05ea6a.png │ ├── 373692488-5e861e22-a969-4eae-aa05-ecd9a339e80d.png │ └── 373695908-341112d4-dac0-488f-961c-9b389396d289.png ├── tinny_CoolS.svg ├── tiny_icon.svg ├── tiny_icon_old.svg └── tiny_placeholder.svg ├── new-player ├── .gitignore ├── assets │ ├── logo-icon.png │ └── logo-styles.css ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── debug │ └── res │ │ └── mipmap │ │ └── thumbnail_preview.jpg │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── net │ │ │ └── newpipe │ │ │ └── newplayer │ │ │ ├── NewPlayer.kt │ │ │ ├── NewPlayerImpl.kt │ │ │ ├── data │ │ │ ├── Chapter.kt │ │ │ ├── NewPlayerException.kt │ │ │ ├── PlayMode.kt │ │ │ ├── RepeatMode.kt │ │ │ ├── Stream.kt │ │ │ ├── StreamSelection.kt │ │ │ ├── StreamTrack.kt │ │ │ ├── Subtitle.kt │ │ │ └── VideoSize.kt │ │ │ ├── logic │ │ │ ├── AutoStreamSelector.kt │ │ │ ├── ConstraintStreamSelector.kt │ │ │ ├── MediaSourceBuilder.kt │ │ │ ├── StreamExceptionResponse.kt │ │ │ └── TrackUtils.kt │ │ │ ├── repository │ │ │ ├── CachingRepository.kt │ │ │ ├── DelayTestRepository.kt │ │ │ ├── MediaRepository.kt │ │ │ ├── MultiRepository.kt │ │ │ ├── PlaceHolderRepository.kt │ │ │ └── PrefetchingRepository.kt │ │ │ ├── service │ │ │ ├── MediaNotification.kt │ │ │ ├── NewPlayerNotificationCustomCommands.kt │ │ │ └── NewPlayerService.kt │ │ │ ├── ui │ │ │ ├── ContentScale.kt │ │ │ ├── LoadingPlaceholder.kt │ │ │ ├── NewPlayerUI.kt │ │ │ ├── NewPlayerView.kt │ │ │ ├── audioplayer │ │ │ │ ├── AudioPlaybackControllerUI.kt │ │ │ │ ├── AudioPlayerEmbeddedUI.kt │ │ │ │ ├── AudioPlayerUI.kt │ │ │ │ ├── BottomUI.kt │ │ │ │ ├── CoverArtUI.kt │ │ │ │ ├── LandscapeLayout.kt │ │ │ │ ├── PortraitLayout.kt │ │ │ │ ├── ProgressUi.kt │ │ │ │ └── TitleView.kt │ │ │ ├── common │ │ │ │ ├── LanguageMenu.kt │ │ │ │ ├── NewPlayerSeeker.kt │ │ │ │ ├── NotYetImplementedToast.kt │ │ │ │ ├── PlaylistControllButtons.kt │ │ │ │ ├── RememberHapticFeedback.kt │ │ │ │ ├── ThumbPreview.kt │ │ │ │ └── utils.kt │ │ │ ├── seeker │ │ │ │ ├── Seeker.kt │ │ │ │ ├── SeekerDefaults.kt │ │ │ │ ├── SeekerState.kt │ │ │ │ └── SeekerUtils.kt │ │ │ ├── selection_ui │ │ │ │ ├── ChapterItem.kt │ │ │ │ ├── ChapterSelectTopBar.kt │ │ │ │ ├── ChapterSelectUI.kt │ │ │ │ ├── StreamItem.kt │ │ │ │ ├── StreamSelectTopBar.kt │ │ │ │ └── StreamSelectUI.kt │ │ │ ├── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ └── videoplayer │ │ │ │ ├── GestureUI.kt │ │ │ │ ├── PlaySurface.kt │ │ │ │ ├── VideoPlayerControllerUI.kt │ │ │ │ ├── VideoPlayerUI.kt │ │ │ │ ├── controller │ │ │ │ ├── BottomUI.kt │ │ │ │ ├── CenterUI.kt │ │ │ │ ├── Menu.kt │ │ │ │ └── TopUI.kt │ │ │ │ ├── gesture_ui │ │ │ │ ├── EmbeddedGestureUI.kt │ │ │ │ ├── FadedAnimationForSeekFeedback.kt │ │ │ │ ├── FastSeekVisualFeedback.kt │ │ │ │ ├── FullscreenGestureUI.kt │ │ │ │ ├── GestureSurface.kt │ │ │ │ ├── TouchedPosition.kt │ │ │ │ └── VolumeCircle.kt │ │ │ │ └── pip │ │ │ │ ├── PipParams.kt │ │ │ │ └── SupportsPiP.kt │ │ │ └── uiModel │ │ │ ├── EmbeddedUiConfig.kt │ │ │ ├── InternalNewPlayerViewModel.kt │ │ │ ├── NewPlayerUIState.kt │ │ │ ├── NewPlayerViewModel.kt │ │ │ ├── NewPlayerViewModelDummy.kt │ │ │ ├── NewPlayerViewModelImpl.kt │ │ │ └── UIModeState.kt │ └── res │ │ ├── drawable │ │ ├── close_24px.xml │ │ ├── ic_play_seek_triangle.xml │ │ ├── new_player_tiny_icon.xml │ │ └── tiny_placeholder.xml │ │ ├── layout │ │ ├── video_player_framgent.xml │ │ └── video_player_view.xml │ │ └── values │ │ ├── language_identifier.xml │ │ └── strings.xml │ └── test │ └── java │ └── net │ └── newpipe │ └── newplayer │ ├── NewPlayerImpltest.kt │ ├── repository │ ├── CachingRepositoryTest.kt │ ├── MockMediaRepository.kt │ └── PrefetchingRepositoryTest.kt │ └── uiModel │ └── NewPlayerViewModelImpltest.kt ├── settings.gradle.kts └── test-app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src └── main ├── AndroidManifest.xml ├── java └── net │ └── newpipe │ └── newplayer │ └── testapp │ ├── MainActivity.kt │ ├── NewPlayerApp.kt │ ├── NewPlayerComponent.kt │ ├── TestMediaRepository.kt │ └── streamErrorHandler.kt └── res ├── drawable-v24 └── ic_launcher_foreground.xml ├── drawable ├── headphones.xml ├── ic_launcher_background.xml ├── pip.xml └── tinny_cools.xml ├── layout-land └── activity_main.xml ├── layout ├── activity_main.xml └── buttons.xml ├── mipmap-anydpi-v26 ├── ic_launcher.xml └── ic_launcher_round.xml ├── mipmap-hdpi ├── ic_launcher.webp └── ic_launcher_round.webp ├── mipmap-mdpi ├── ic_launcher.webp └── ic_launcher_round.webp ├── mipmap-xhdpi ├── ic_launcher.webp └── ic_launcher_round.webp ├── mipmap-xxhdpi ├── ic_launcher.webp └── ic_launcher_round.webp ├── mipmap-xxxhdpi ├── ic_launcher.webp └── ic_launcher_round.webp ├── values-night └── themes.xml ├── values ├── colors.xml ├── strings.xml ├── test_streams.xml └── themes.xml └── xml ├── backup_rules.xml └── data_extraction_rules.xml /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: 7 | - dev 8 | - master 9 | - refactor 10 | - release** 11 | paths-ignore: 12 | - 'README.md' 13 | - 'doc/**' 14 | - 'fastlane/**' 15 | - 'assets/**' 16 | - '.github/**/*.md' 17 | - '.github/FUNDING.yml' 18 | - '.github/ISSUE_TEMPLATE/**' 19 | push: 20 | branches: 21 | - dev 22 | - master 23 | paths-ignore: 24 | - 'README.md' 25 | - 'doc/**' 26 | - 'fastlane/**' 27 | - 'assets/**' 28 | - '.github/**/*.md' 29 | - '.github/FUNDING.yml' 30 | - '.github/ISSUE_TEMPLATE/**' 31 | 32 | jobs: 33 | build-and-test-jvm: 34 | runs-on: ubuntu-latest 35 | 36 | permissions: 37 | contents: read 38 | 39 | steps: 40 | - uses: actions/checkout@v4 41 | - uses: gradle/wrapper-validation-action@v2 42 | 43 | - name: create and checkout branch 44 | # push events already checked out the branch 45 | if: github.event_name == 'pull_request' 46 | env: 47 | BRANCH: ${{ github.head_ref }} 48 | run: git checkout -B "$BRANCH" 49 | 50 | - name: set up JDK 21 51 | uses: actions/setup-java@v4 52 | with: 53 | java-version: 21 54 | distribution: "temurin" 55 | cache: 'gradle' 56 | 57 | - name: Build debug APK and run jvm tests 58 | run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint 59 | 60 | test-android: 61 | runs-on: ubuntu-latest 62 | timeout-minutes: 20 63 | strategy: 64 | matrix: 65 | include: 66 | - api-level: 21 67 | target: default 68 | arch: x86 69 | - api-level: 35 70 | target: google_apis # emulator API 33 only exists with Google APIs 71 | arch: x86_64 72 | 73 | permissions: 74 | contents: read 75 | 76 | steps: 77 | - uses: actions/checkout@v4 78 | 79 | - name: Enable KVM 80 | run: | 81 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules 82 | sudo udevadm control --reload-rules 83 | sudo udevadm trigger --name-match=kvm 84 | 85 | - name: set up JDK 17 86 | uses: actions/setup-java@v4 87 | with: 88 | java-version: 17 89 | distribution: "temurin" 90 | cache: 'gradle' 91 | 92 | - name: Run android tests 93 | uses: reactivecircus/android-emulator-runner@v2 94 | with: 95 | api-level: ${{ matrix.api-level }} 96 | target: ${{ matrix.target }} 97 | arch: ${{ matrix.arch }} 98 | script: ./gradlew connectedCheck --stacktrace 99 | 100 | - name: Upload test report when tests fail # because the printed out stacktrace (console) is too short, see also #7553 101 | uses: actions/upload-artifact@v4 102 | if: failure() 103 | with: 104 | name: android-test-report-api${{ matrix.api-level }} 105 | path: app/build/reports/androidTests/connected/** 106 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Build and deploy Kotlin docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | permissions: 9 | # The generated docs are written to the `gh-pages` branch. 10 | contents: write 11 | 12 | jobs: 13 | build-and-deploy-docs: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: set up JDK 21 19 | uses: actions/setup-java@v4 20 | with: 21 | java-version: '21' 22 | distribution: 'temurin' 23 | 24 | - name: Cache Gradle dependencies 25 | uses: actions/cache@v4 26 | with: 27 | path: ~/.gradle/caches 28 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} 29 | restore-keys: ${{ runner.os }}-gradle 30 | 31 | - name: Build DokkaDocs 32 | run: ./gradlew dokkaHtml 33 | 34 | - name: Deploy DokkaDocs 35 | uses: peaceiris/actions-gh-pages@v4 36 | with: 37 | github_token: ${{ secrets.GITHUB_TOKEN }} 38 | publish_dir: ./new-player/build/dokka/html 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | .kotlin 12 | .vscode 13 | -------------------------------------------------------------------------------- /.idea/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.kotlin/errors/errors-1721380185468.log: -------------------------------------------------------------------------------- 1 | kotlin version: 2.0.20-Beta2 2 | error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: 3 | 1. Kotlin compile daemon is ready 4 | 5 | -------------------------------------------------------------------------------- /.kotlin/errors/errors-1724916413735.log: -------------------------------------------------------------------------------- 1 | kotlin version: 2.0.20-Beta2 2 | error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: 3 | 1. Kotlin compile daemon is ready 4 | 5 | -------------------------------------------------------------------------------- /.kotlin/errors/errors-1725028164833.log: -------------------------------------------------------------------------------- 1 | kotlin version: 2.0.20-Beta2 2 | error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: 3 | 1. Kotlin compile daemon is ready 4 | 5 | -------------------------------------------------------------------------------- /.kotlin/errors/errors-1727005936545.log: -------------------------------------------------------------------------------- 1 | kotlin version: 2.0.20-Beta2 2 | error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: 3 | 1. Kotlin compile daemon is ready 4 | 5 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.dokka.gradle.DokkaTask 2 | import org.jetbrains.dokka.base.DokkaBase 3 | import org.jetbrains.dokka.base.DokkaBaseConfiguration 4 | 5 | /* NewPlayer 6 | * 7 | * @author Christian Schabesberger 8 | * 9 | * Copyright (C) NewPipe e.V. 2024 10 | * 11 | * NewPlayer is free software: you can redistribute it and/or modify 12 | * it under the terms of the GNU General Public License as published by 13 | * the Free Software Foundation, either version 3 of the License, or 14 | * (at your option) any later version. 15 | * 16 | * NewPlayer is distributed in the hope that it will be useful, 17 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | * GNU General Public License for more details. 20 | * 21 | * You should have received a copy of the GNU General Public License 22 | * along with NewPlayer. If not, see . 23 | */ 24 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 25 | plugins { 26 | alias(libs.plugins.android.application) apply false 27 | alias(libs.plugins.jetbrains.kotlin.android) apply false 28 | alias(libs.plugins.androidHilt) apply false 29 | alias(libs.plugins.kotlinAndroidKsp) apply false 30 | alias(libs.plugins.kotlinParcelize) apply false 31 | alias(libs.plugins.composeCompiler) apply false 32 | alias(libs.plugins.android.library) apply false 33 | alias(libs.plugins.dokka.base) apply false 34 | `maven-publish` 35 | } 36 | 37 | afterEvaluate { 38 | publishing { 39 | publications { 40 | create("release") { 41 | groupId = "com.github.the-scrabi" 42 | artifactId = "NewPlayer" 43 | version = "0.1-DEVEL" 44 | } 45 | } 46 | } 47 | } 48 | 49 | 50 | buildscript { 51 | dependencies { 52 | classpath(libs.dokka.base) 53 | classpath(libs.dokka.android) 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. For more details, visit 12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamNewPipe/NewPlayer/2bb7e323446a38486bc6c1151e84bba83422bd63/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jul 02 10:12:32 CEST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk21 3 | -------------------------------------------------------------------------------- /misc/TwoStagedTrackSelection/FirstStage_constrainttrackselector.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamNewPipe/NewPlayer/2bb7e323446a38486bc6c1151e84bba83422bd63/misc/TwoStagedTrackSelection/FirstStage_constrainttrackselector.pdf -------------------------------------------------------------------------------- /misc/TwoStagedTrackSelection/TwoStagedStreamSelection_overview.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamNewPipe/NewPlayer/2bb7e323446a38486bc6c1151e84bba83422bd63/misc/TwoStagedTrackSelection/TwoStagedStreamSelection_overview.pdf -------------------------------------------------------------------------------- /misc/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /misc/icon_old.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 24 | 25 | 65 | -------------------------------------------------------------------------------- /misc/logo_shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamNewPipe/NewPlayer/2bb7e323446a38486bc6c1151e84bba83422bd63/misc/logo_shadow.png -------------------------------------------------------------------------------- /misc/screenshots/373685724-42609e51-6bf7-4008-b084-a59ce111f3c1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamNewPipe/NewPlayer/2bb7e323446a38486bc6c1151e84bba83422bd63/misc/screenshots/373685724-42609e51-6bf7-4008-b084-a59ce111f3c1.png -------------------------------------------------------------------------------- /misc/screenshots/373686583-1164cf7c-66eb-48be-aeda-55e6e6294cf1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamNewPipe/NewPlayer/2bb7e323446a38486bc6c1151e84bba83422bd63/misc/screenshots/373686583-1164cf7c-66eb-48be-aeda-55e6e6294cf1.png -------------------------------------------------------------------------------- /misc/screenshots/373688583-9011749c-3aec-4bf7-a368-40000c84f8e3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamNewPipe/NewPlayer/2bb7e323446a38486bc6c1151e84bba83422bd63/misc/screenshots/373688583-9011749c-3aec-4bf7-a368-40000c84f8e3.png -------------------------------------------------------------------------------- /misc/screenshots/373689058-9fc27dfd-7f89-48de-b0ff-cd6e9bd4fdbd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamNewPipe/NewPlayer/2bb7e323446a38486bc6c1151e84bba83422bd63/misc/screenshots/373689058-9fc27dfd-7f89-48de-b0ff-cd6e9bd4fdbd.png -------------------------------------------------------------------------------- /misc/screenshots/373690788-a7af6db0-eac2-4913-8f60-3d621c5afd9f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamNewPipe/NewPlayer/2bb7e323446a38486bc6c1151e84bba83422bd63/misc/screenshots/373690788-a7af6db0-eac2-4913-8f60-3d621c5afd9f.png -------------------------------------------------------------------------------- /misc/screenshots/373690876-9b96ae22-d537-4b49-ac1c-4549a94bebcb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamNewPipe/NewPlayer/2bb7e323446a38486bc6c1151e84bba83422bd63/misc/screenshots/373690876-9b96ae22-d537-4b49-ac1c-4549a94bebcb.png -------------------------------------------------------------------------------- /misc/screenshots/373691456-4aaff87d-dbf8-4877-866b-60e6fc05ea6a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamNewPipe/NewPlayer/2bb7e323446a38486bc6c1151e84bba83422bd63/misc/screenshots/373691456-4aaff87d-dbf8-4877-866b-60e6fc05ea6a.png -------------------------------------------------------------------------------- /misc/screenshots/373692488-5e861e22-a969-4eae-aa05-ecd9a339e80d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamNewPipe/NewPlayer/2bb7e323446a38486bc6c1151e84bba83422bd63/misc/screenshots/373692488-5e861e22-a969-4eae-aa05-ecd9a339e80d.png -------------------------------------------------------------------------------- /misc/screenshots/373695908-341112d4-dac0-488f-961c-9b389396d289.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamNewPipe/NewPlayer/2bb7e323446a38486bc6c1151e84bba83422bd63/misc/screenshots/373695908-341112d4-dac0-488f-961c-9b389396d289.png -------------------------------------------------------------------------------- /misc/tinny_CoolS.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 63 | -------------------------------------------------------------------------------- /misc/tiny_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 | 25 | 26 | 38 | 56 | 58 | 61 | 65 | 66 | 67 | 71 | 74 | 78 | 79 | 82 | 86 | 87 | 90 | 93 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /misc/tiny_icon_old.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 24 | 25 | 65 | -------------------------------------------------------------------------------- /misc/tiny_placeholder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 41 | 48 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /new-player/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /new-player/assets/logo-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamNewPipe/NewPlayer/2bb7e323446a38486bc6c1151e84bba83422bd63/new-player/assets/logo-icon.png -------------------------------------------------------------------------------- /new-player/assets/logo-styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | :root { 6 | --dokka-logo-image-url: url('../images/logo-icon.png'); 7 | --dokka-logo-height: 50px; 8 | --dokka-logo-width: 50px; 9 | } 10 | -------------------------------------------------------------------------------- /new-player/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.dokka.base.DokkaBase 2 | import org.jetbrains.dokka.base.DokkaBaseConfiguration 3 | import org.jetbrains.dokka.gradle.DokkaTask 4 | 5 | plugins { 6 | alias(libs.plugins.android.library) 7 | alias(libs.plugins.jetbrains.kotlin.android) 8 | alias(libs.plugins.kotlinAndroidKsp) 9 | alias(libs.plugins.androidHilt) 10 | alias(libs.plugins.kotlinParcelize) 11 | alias(libs.plugins.composeCompiler) 12 | alias(libs.plugins.dokka.base) 13 | } 14 | 15 | android { 16 | namespace = "net.newpipe.newplayer" 17 | compileSdk = 35 18 | 19 | buildFeatures { 20 | compose = true 21 | } 22 | composeOptions { 23 | kotlinCompilerExtensionVersion = "1.5.1" 24 | } 25 | 26 | defaultConfig { 27 | minSdk = 21 28 | aarMetadata { 29 | minCompileSdk = 21 30 | } 31 | 32 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 33 | consumerProguardFiles("consumer-rules.pro") 34 | } 35 | 36 | buildTypes { 37 | release { 38 | isMinifyEnabled = false 39 | proguardFiles( 40 | getDefaultProguardFile("proguard-android-optimize.txt"), 41 | "proguard-rules.pro" 42 | ) 43 | } 44 | } 45 | compileOptions { 46 | sourceCompatibility = JavaVersion.VERSION_1_8 47 | targetCompatibility = JavaVersion.VERSION_1_8 48 | } 49 | kotlinOptions { 50 | jvmTarget = "1.8" 51 | } 52 | } 53 | 54 | dependencies { 55 | implementation(libs.androidx.core.ktx) 56 | implementation(libs.androidx.appcompat) 57 | implementation(libs.material) 58 | implementation(libs.androidx.constraintlayout) 59 | implementation(libs.androidx.material3) 60 | implementation(libs.androidx.ui.tooling) 61 | implementation(libs.androidx.material.icons.extended.android) 62 | implementation(libs.androidx.media3.exoplayer) 63 | implementation(libs.androidx.media3.ui) 64 | implementation(libs.hilt.android) 65 | implementation(libs.androidx.lifecycle.viewmodel.compose) 66 | implementation(libs.androidx.foundation) 67 | implementation(libs.androidx.fragment.ktx) 68 | implementation(libs.androidx.lifecycle.runtime.ktx) 69 | implementation(platform(libs.androidx.compose.bom)) 70 | implementation(libs.androidx.ui) 71 | implementation(libs.androidx.ui.graphics) 72 | implementation(libs.androidx.ui.tooling.preview) 73 | implementation(libs.androidx.hilt.navigation.compose) 74 | implementation(libs.androidx.media3.common) 75 | implementation(libs.coil.compose) 76 | implementation(libs.reorderable) 77 | implementation(libs.androidx.media3.session) 78 | implementation(libs.androidx.media3.exoplayer.dash) 79 | implementation(libs.androidx.adaptive.android) 80 | 81 | ksp(libs.hilt.android.compiler) 82 | ksp(libs.androidx.hilt.compiler) 83 | 84 | testImplementation(libs.junit) 85 | testImplementation(libs.kotlinx.coroutines.test) 86 | testImplementation(libs.mockk) 87 | 88 | androidTestImplementation(libs.androidx.junit) 89 | androidTestImplementation(libs.androidx.espresso.core) 90 | } 91 | 92 | 93 | tasks.withType().configureEach { 94 | pluginConfiguration { 95 | customAssets = listOf(file("assets/logo-icon.png")) 96 | customStyleSheets = listOf(file("assets/logo-styles.css")) 97 | footerMessage = "(c) 2024 NewPipe e.V." 98 | } 99 | } -------------------------------------------------------------------------------- /new-player/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamNewPipe/NewPlayer/2bb7e323446a38486bc6c1151e84bba83422bd63/new-player/consumer-rules.pro -------------------------------------------------------------------------------- /new-player/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /new-player/src/debug/res/mipmap/thumbnail_preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamNewPipe/NewPlayer/2bb7e323446a38486bc6c1151e84bba83422bd63/new-player/src/debug/res/mipmap/thumbnail_preview.jpg -------------------------------------------------------------------------------- /new-player/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/data/Chapter.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.data 22 | 23 | import android.net.Uri 24 | 25 | data class Chapter(val chapterStartInMs: Long, val chapterTitle: String?, val thumbnail: Uri?) 26 | 27 | -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/data/NewPlayerException.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.data 22 | 23 | /** 24 | * An Exception, but from NewPlayer. 25 | */ 26 | class NewPlayerException : Exception { 27 | constructor(message: String) : super(message) 28 | constructor(message: String, cause: Throwable) : super(message, cause) 29 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/data/PlayMode.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | 22 | package net.newpipe.newplayer.data 23 | 24 | import net.newpipe.newplayer.ui.NewPlayerUI 25 | 26 | /** 27 | * Depicts the playback mode that NewPlayer is currently in. 28 | * Or in other words this tells how NewPlayer is displaying media. 29 | */ 30 | enum class PlayMode { 31 | 32 | /** 33 | * NewPlayer is currently idling and is not displaying anything. 34 | * If NewPlayer is in this mode [NewPlayerUI] will show a loading circle on a black background. 35 | * If this should not be visible please hide ore remove [NewPlayerUI] when NewPlayer switches 36 | * to IDLE mode. 37 | */ 38 | IDLE, 39 | 40 | /** 41 | * Video is played in an embedded view. 42 | * [See Screenshot](https://github.com/TeamNewPipe/NewPlayer/blob/master/misc/screenshots/373686583-1164cf7c-66eb-48be-aeda-55e6e6294cf1.png) 43 | */ 44 | EMBEDDED_VIDEO, 45 | 46 | /** 47 | * Video is played in fullscreen mode. 48 | * [See Screenshot](https://github.com/TeamNewPipe/NewPlayer/blob/master/misc/screenshots/373685724-42609e51-6bf7-4008-b084-a59ce111f3c1.png) 49 | */ 50 | FULLSCREEN_VIDEO, 51 | 52 | /** 53 | * Video is displayed in Picture in Picture mode 54 | * [see Screenshot](https://github.com/TeamNewPipe/NewPlayer/blob/master/misc/screenshots/373691456-4aaff87d-dbf8-4877-866b-60e6fc05ea6a.png) 55 | */ 56 | PIP, 57 | 58 | /** 59 | * TODO: Obsolete and does not work. Remove this!!! 60 | */ 61 | BACKGROUND_VIDEO, 62 | 63 | /** 64 | * TODO: Obsolete and does not work. Remove this!!! 65 | */ 66 | BACKGROUND_AUDIO, 67 | 68 | /** 69 | * Plays a Video/Audio stream while showing the audio player ui. 70 | * [See Screenshot](https://github.com/TeamNewPipe/NewPlayer/blob/master/misc/screenshots/373688583-9011749c-3aec-4bf7-a368-40000c84f8e3.png) 71 | * [See Screenshot in landscape](https://github.com/TeamNewPipe/NewPlayer/blob/master/misc/screenshots/373689058-9fc27dfd-7f89-48de-b0ff-cd6e9bd4fdbd.png) 72 | */ 73 | FULLSCREEN_AUDIO, 74 | 75 | /** 76 | * Shows the embedded UI for Audio playback of a Video/Audio stream. 77 | */ 78 | EMBEDDED_AUDIO 79 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/data/RepeatMode.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.data 22 | 23 | 24 | import net.newpipe.newplayer.data.PlayMode.IDLE 25 | /** 26 | * The playlist repeat mode. 27 | */ 28 | enum class RepeatMode { 29 | /** 30 | * Don't repeat. Quit playback and switch to [IDLE] mode after being done playing the active 31 | * playlist. 32 | */ 33 | DO_NOT_REPEAT, 34 | 35 | /** 36 | * Repeats the currently active playlist after playing the last item of the playlist. 37 | */ 38 | REPEAT_ALL, 39 | 40 | /** 41 | * Keeps repeating the current item of a playlist. 42 | */ 43 | REPEAT_ONE 44 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/data/Stream.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.data 22 | 23 | import android.net.Uri 24 | 25 | /** 26 | * A stream represents one actual video stream that is associated with a video. 27 | * Each stream has its own URI (and therefore correspond to one individual video container). 28 | * Each stream can contain multiple audio/video tracks. 29 | * 30 | * @param item The item the stream belongs to. 31 | * @param streamUri the URI of the stream. 32 | * @param streamTracks the tracks that the stream contains 33 | * @param mimeType The mime type of the stream. This may only be set if ExoPlayer might not be 34 | * able to infer the type of of the stream from the Uri itself. 35 | * @param isDashOrHls depicts wather its a dynamic stream or not. 36 | */ 37 | data class Stream( 38 | val item: String, 39 | val streamUri: Uri, 40 | val streamTracks: List, 41 | val mimeType: String? = null, 42 | val isDashOrHls: Boolean = false 43 | ) { 44 | 45 | /** 46 | * The list of audio languages provided by the stream. 47 | */ 48 | val languages: List 49 | get() = streamTracks.filterIsInstance().mapNotNull { it.language } 50 | 51 | 52 | val hasAudioTracks: Boolean 53 | get() { 54 | streamTracks.forEach { if (it is AudioStreamTrack) return true } 55 | return false 56 | } 57 | 58 | val hasVideoTracks: Boolean 59 | get() { 60 | streamTracks.forEach { if (it is VideoStreamTrack) return true } 61 | return false 62 | } 63 | 64 | 65 | val videoStreamTracks: List 66 | get() = streamTracks.filterIsInstance() 67 | 68 | val audioStreamTrack: List 69 | get() = streamTracks.filterIsInstance() 70 | 71 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/data/StreamSelection.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.data 22 | 23 | import androidx.media3.exoplayer.source.MergingMediaSource 24 | import net.newpipe.newplayer.logic.TrackUtils 25 | 26 | /** 27 | * A selection of streams that should be used for playback. 28 | * This is used by the stream selection algorithm to depict which streams should be used 29 | * to build a MediaSource from and thus forward to the actual ExoPlayer. 30 | */ 31 | interface StreamSelection { 32 | val item: String 33 | 34 | val tracks : List 35 | val hasVideoTracks:Boolean 36 | val hasAudioTracks:Boolean 37 | val isDynamic:Boolean 38 | } 39 | 40 | /** 41 | * This is used if only one single stream should be forwarded to ExoPlayer. 42 | * This can be either a DASH/HLS stream or a progressive stream that has all the required 43 | * tracks already muxed together. 44 | */ 45 | data class SingleSelection( 46 | val stream: Stream 47 | ) : StreamSelection { 48 | override val item: String 49 | get() = stream.item 50 | 51 | override val tracks: List 52 | get() = stream.streamTracks 53 | 54 | override val hasVideoTracks: Boolean 55 | get() = stream.hasVideoTracks 56 | 57 | override val hasAudioTracks: Boolean 58 | get() = stream.hasVideoTracks 59 | 60 | override val isDynamic: Boolean 61 | get() = stream.isDashOrHls 62 | } 63 | 64 | /** 65 | * This can be used if tracks from multiple streams should be forwarded to ExoPlayer, so 66 | * ExoPlayer can mux them together. 67 | * This StreamSelection will be made into a [MergingMediaSource]. 68 | * This stream selection will not depict which of the tracks contained in the StreamSelection 69 | * should be muxed together by ExoPlayer. This MultiSelection only depicts that at least all the 70 | * tracks that should be played are contained. 71 | * 72 | * The information to pick the actual tracks out of the available tracks within this selection 73 | * bust be given to ExoPlayer through another mechanism. (You see this is still TODO). 74 | */ 75 | data class MultiSelection( 76 | val streams: List 77 | ) : StreamSelection { 78 | 79 | override val item: String 80 | get() = streams[0].item 81 | 82 | override val tracks: List 83 | get() { 84 | val allTracks = mutableListOf() 85 | streams.forEach { allTracks.addAll(it.streamTracks) } 86 | return allTracks 87 | } 88 | 89 | override val hasVideoTracks: Boolean 90 | get() = TrackUtils.hasVideoTracks(streams) 91 | 92 | override val hasAudioTracks: Boolean 93 | get() = TrackUtils.hasAudioTracks(streams) 94 | 95 | override val isDynamic: Boolean 96 | get() = TrackUtils.hasDynamicStreams(streams) 97 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/data/StreamTrack.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.data 22 | 23 | /** 24 | * Media3 does not provide a class to represent individual tracks. So here we go. 25 | */ 26 | sealed interface StreamTrack { 27 | val fileFormat: String 28 | 29 | fun toShortIdentifierString(): String 30 | fun toLongIdentifierString(): String 31 | 32 | companion object { 33 | /** [Comparator] for StreamTracks, sorts VideoStreamTracks before AudioStreamTracks */ 34 | fun compareResolution(t1: StreamTrack, t2: StreamTrack) = 35 | when { 36 | // video comes before audio 37 | t1 is VideoStreamTrack && t2 is AudioStreamTrack -> -1 38 | // audio comes after video 39 | t1 is AudioStreamTrack && t2 is VideoStreamTrack -> 1 40 | // better audio/video first 41 | t1 is VideoStreamTrack && t2 is VideoStreamTrack -> -VideoStreamTrack.compareResolutions(t1, t2) 42 | t1 is AudioStreamTrack && t2 is AudioStreamTrack -> -AudioStreamTrack.compareResolutions(t1, t2) 43 | // should not happen 44 | else -> 0 45 | } 46 | } 47 | } 48 | 49 | /** 50 | * A track representing a video track. 51 | */ 52 | data class VideoStreamTrack( 53 | val width: Int, 54 | val height: Int, 55 | val frameRate: Int, 56 | override val fileFormat: String, 57 | ) : StreamTrack { 58 | 59 | override fun toShortIdentifierString() = 60 | "${if (width < height) width else height}p${if (frameRate > 30) frameRate else ""}" 61 | 62 | override fun toLongIdentifierString() = "$fileFormat ${toShortIdentifierString()}" 63 | 64 | companion object { 65 | /** 66 | * [Comparator] for VideoStreamTrack resolutions 67 | */ 68 | fun compareResolutions(a: VideoStreamTrack, b: VideoStreamTrack): Int { 69 | val diff = a.width * a.height - b.width * b.height 70 | if (diff != 0) { 71 | return diff 72 | } 73 | return a.frameRate - b.frameRate 74 | } 75 | } 76 | 77 | override fun toString() = """ 78 | VideoStreamTrack { 79 | width = $width 80 | height = $height 81 | frameRate = $frameRate 82 | fileFormat = $fileFormat 83 | } 84 | """.trimIndent() 85 | 86 | } 87 | 88 | /** 89 | * A track representing an audio track. 90 | */ 91 | data class AudioStreamTrack( 92 | val bitrate: Int, 93 | override val fileFormat: String, 94 | val language: String? = null 95 | ) : StreamTrack { 96 | 97 | override fun toShortIdentifierString() = 98 | if (bitrate < 1000) "${bitrate}bps" else "${bitrate / 1000}kbps" 99 | 100 | override fun toLongIdentifierString() = "$fileFormat ${toShortIdentifierString()}" 101 | 102 | companion object { 103 | /** [Comparator] for AudioStreamTrack bitrates */ 104 | fun compareResolutions(a: AudioStreamTrack, b: AudioStreamTrack) = 105 | a.bitrate - b.bitrate 106 | } 107 | 108 | override fun toString() = """ 109 | AudioStreamTrack { 110 | bitrate = $bitrate 111 | language = $language 112 | fileFormat = $fileFormat 113 | } 114 | """.trimIndent() 115 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/data/Subtitle.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.data 22 | 23 | import android.net.Uri 24 | 25 | /** 26 | * TODO 27 | */ 28 | data class Subtitle( 29 | val uri: Uri, 30 | val identifier: String 31 | ) 32 | -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/data/VideoSize.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | 22 | package net.newpipe.newplayer.data 23 | 24 | /** 25 | * This class depicts video sizes. A Media3 implementation ([androidx.media3.common.VideoSize]) 26 | * could have been used, however because of the pixelWidthHeightRatio stuff I wanted a tool that 27 | * I can control better. 28 | * 29 | * @param width depicts the width of the video with which it is encoded with 30 | * (not with which it is played back with). 31 | * @param height depicts the height of the video with which it is encoded with 32 | * (not with which it is played back with). 33 | * @param pixelWidthHeightRatio the ratio of each individual pixel. Normally it's 1 but some 34 | * (older) media.ccc videos go wonky. 35 | * 36 | * @hide 37 | */ 38 | /** @hide */ 39 | internal data class VideoSize( 40 | 41 | val width: Int, 42 | val height: Int, 43 | /// The width/height ratio of a single pixel 44 | val pixelWidthHeightRatio: Float 45 | ) : Comparable { 46 | 47 | override fun compareTo(other: VideoSize) = width * height - other.width * other.height 48 | 49 | fun getRatio() = 50 | (width * pixelWidthHeightRatio) / height 51 | 52 | override fun toString() = 53 | "VideoSize(width = $width, height = $height, pixelRatio = $pixelWidthHeightRatio, ratio = ${getRatio()})" 54 | 55 | companion object { 56 | val DEFAULT = VideoSize(0, 0, 1F) 57 | 58 | fun fromMedia3VideoSize(videoSize: androidx.media3.common.VideoSize) = 59 | VideoSize(videoSize.width, videoSize.height, videoSize.pixelWidthHeightRatio) 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/logic/AutoStreamSelector.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.logic 22 | 23 | import net.newpipe.newplayer.data.MultiSelection 24 | import net.newpipe.newplayer.data.NewPlayerException 25 | import net.newpipe.newplayer.data.SingleSelection 26 | import net.newpipe.newplayer.data.Stream 27 | import net.newpipe.newplayer.data.StreamSelection 28 | import net.newpipe.newplayer.logic.TrackUtils.getDynamicStreams 29 | import net.newpipe.newplayer.logic.TrackUtils.hasVideoTracks 30 | import net.newpipe.newplayer.logic.TrackUtils.tryAndGetMedianAudioOnlyTracks 31 | import net.newpipe.newplayer.logic.TrackUtils.tryAndGetMedianCombinedVideoAndAudioTracks 32 | import net.newpipe.newplayer.logic.TrackUtils.tryAndGetMedianVideoOnlyTracks 33 | 34 | /** 35 | * TODO: This whole class is just a concept. this should really be finished at some point. 36 | * 37 | * @hide 38 | */ 39 | /** @hide */ 40 | internal class AutoStreamSelector( 41 | /** 42 | * Must be in IETF-BCP-47 format 43 | * This is a soft constraint. If no language can be found in the available tracks 44 | * that fits on of the preferred languages, a default language is picked. 45 | */ 46 | val preferredLanguages: List, 47 | 48 | /** 49 | * This is a hard constraint. If not null a stream with this language must be selected, 50 | * otherwise the automatic stream selection fails. 51 | * Setting this will override the [preferredLanguages]. 52 | */ 53 | val streamLanguageConstraint: String? 54 | ) { 55 | 56 | 57 | /** @hide */ 58 | internal fun selectStreamAutomatically( 59 | availableStreams: List, 60 | ): StreamSelection { 61 | 62 | 63 | // filter for best fitting language stream variants 64 | 65 | val bestFittingLanguage = 66 | TrackUtils.getBestLanguageFit(availableStreams, preferredLanguages) 67 | 68 | val availableInPreferredLanguage = 69 | if (bestFittingLanguage != null) TrackUtils.filtersByLanguage( 70 | availableStreams, bestFittingLanguage 71 | ) 72 | else { 73 | emptyList() 74 | } 75 | 76 | 77 | // is it a video stream or a pure audio stream? 78 | if (hasVideoTracks(availableStreams)) { 79 | 80 | // first: try and get a dynamic stream variant 81 | val dynamicStreams = getDynamicStreams(availableInPreferredLanguage) 82 | if (dynamicStreams.isNotEmpty()) { 83 | return SingleSelection(dynamicStreams[0]) 84 | } 85 | 86 | // second: try and get separate audio and video stream variants 87 | 88 | val videoOnlyStream = tryAndGetMedianVideoOnlyTracks(availableStreams) 89 | 90 | 91 | if (videoOnlyStream != null) { 92 | 93 | val audioStream = tryAndGetMedianAudioOnlyTracks(availableStreams) 94 | 95 | if (videoOnlyStream != null && audioStream != null) { 96 | return MultiSelection(listOf(videoOnlyStream, audioStream)) 97 | } 98 | } /* if (vdeioOnlyStream != null) */ 99 | 100 | // fourth: try to get a video and audio stream variant with the best fitting identifier 101 | 102 | tryAndGetMedianCombinedVideoAndAudioTracks(availableStreams)?.let { 103 | return SingleSelection(it) 104 | } 105 | 106 | } else { /* if(!hasVideoStreams(availableStreams)) */ 107 | 108 | // first: try to get an audio stream variant with the best fitting identifier 109 | 110 | tryAndGetMedianAudioOnlyTracks(availableStreams)?.let { 111 | return SingleSelection(it) 112 | } 113 | } 114 | 115 | throw NewPlayerException("StreamSelector: No suitable Stream found that.") 116 | } 117 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/logic/ConstraintStreamSelector.kt: -------------------------------------------------------------------------------- 1 | package net.newpipe.newplayer.logic 2 | 3 | import net.newpipe.newplayer.data.Stream 4 | import net.newpipe.newplayer.data.StreamSelection 5 | import net.newpipe.newplayer.data.StreamTrack 6 | 7 | /** 8 | * TODO 9 | * This selector should be used if the uses did pic a specific language and/or a specific 10 | * video resolution. 11 | * 12 | * @hide 13 | */ 14 | /** @hide */ 15 | internal object ConstraintStreamSelector { 16 | fun selectStream( 17 | availableStreams: List, 18 | currentStreamSelection: StreamSelection, 19 | currentlyPlayingTracks: List, 20 | languageConstraint: String?, 21 | trackConstraint: StreamTrack? 22 | ): StreamSelection { 23 | 24 | val availableFilteredByLanguage = if (languageConstraint != null) 25 | TrackUtils.filtersByLanguage(availableStreams, languageConstraint) 26 | else 27 | availableStreams 28 | 29 | 30 | 31 | return currentStreamSelection 32 | } 33 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/logic/MediaSourceBuilder.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.logic 22 | 23 | import androidx.annotation.OptIn 24 | import androidx.media3.common.MediaItem 25 | import androidx.media3.common.util.UnstableApi 26 | import androidx.media3.datasource.HttpDataSource 27 | import androidx.media3.exoplayer.dash.DashMediaSource 28 | import androidx.media3.exoplayer.source.MediaSource 29 | import androidx.media3.exoplayer.source.MergingMediaSource 30 | import androidx.media3.exoplayer.source.ProgressiveMediaSource 31 | import kotlinx.coroutines.flow.MutableSharedFlow 32 | import net.newpipe.newplayer.repository.MediaRepository 33 | import net.newpipe.newplayer.data.MultiSelection 34 | import net.newpipe.newplayer.data.NewPlayerException 35 | import net.newpipe.newplayer.data.SingleSelection 36 | import net.newpipe.newplayer.data.Stream 37 | import net.newpipe.newplayer.data.StreamSelection 38 | 39 | /** 40 | * This class help to transform a [StreamSelection] into a [MediaSource]. 41 | * 42 | * @hide 43 | */ 44 | @OptIn(UnstableApi::class) 45 | /** @hide */ 46 | internal class MediaSourceBuilder 47 | ( 48 | private val repository: MediaRepository, 49 | private val mutableErrorFlow: MutableSharedFlow, 50 | private val httpDataSourceFactory: HttpDataSource.Factory, 51 | ) { 52 | @OptIn(UnstableApi::class) 53 | 54 | /** @hide */ 55 | internal suspend fun buildMediaSource( 56 | streamSelection: StreamSelection, 57 | uniqueId: Long 58 | ): MediaSource { 59 | when (streamSelection) { 60 | is SingleSelection -> { 61 | val mediaItem = toMediaItem(streamSelection.stream, uniqueId) 62 | val mediaItemWithMetadata = addMetadata(mediaItem, streamSelection.item) 63 | return toMediaSource(mediaItemWithMetadata, streamSelection.stream) 64 | } 65 | 66 | is MultiSelection -> { 67 | val mediaItems = ArrayList(streamSelection.streams.map { 68 | toMediaItem( 69 | it, 70 | uniqueId 71 | ) 72 | }) 73 | mediaItems[0] = addMetadata(mediaItems[0], streamSelection.item) 74 | val mediaSources = mediaItems.zip(streamSelection.streams) 75 | .map { toMediaSource(it.first, it.second) } 76 | return MergingMediaSource( 77 | true, true, 78 | *mediaSources.toTypedArray() 79 | ) 80 | } 81 | 82 | else -> { 83 | throw NewPlayerException("Unknown stream selection class: ${streamSelection.javaClass}") 84 | } 85 | } 86 | } 87 | 88 | @OptIn(UnstableApi::class) 89 | private 90 | fun toMediaItem(stream: Stream, uniqueId: Long): MediaItem { 91 | 92 | val mediaItemBuilder = MediaItem.Builder() 93 | .setMediaId(uniqueId.toString()) 94 | .setUri(stream.streamUri) 95 | 96 | if (stream.mimeType != null) { 97 | mediaItemBuilder.setMimeType(stream.mimeType) 98 | } 99 | 100 | val mediaItem = mediaItemBuilder.build() 101 | return mediaItem 102 | } 103 | 104 | @OptIn(UnstableApi::class) 105 | private fun toMediaSource(mediaItem: MediaItem, stream: Stream): MediaSource = 106 | if (stream.isDashOrHls) 107 | DashMediaSource.Factory(httpDataSourceFactory) 108 | .createMediaSource(mediaItem) 109 | else 110 | ProgressiveMediaSource.Factory(httpDataSourceFactory) 111 | .createMediaSource(mediaItem) 112 | 113 | 114 | private suspend fun 115 | addMetadata(mediaItem: MediaItem, item: String): MediaItem { 116 | val mediaItemBuilder = mediaItem.buildUpon() 117 | 118 | try { 119 | val metadata = repository.getMetaInfo(item) 120 | mediaItemBuilder.setMediaMetadata(metadata) 121 | } catch (e: Exception) { 122 | mutableErrorFlow.emit(e) 123 | } 124 | 125 | return mediaItemBuilder.build() 126 | } 127 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/logic/StreamExceptionResponse.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.logic; 22 | 23 | import net.newpipe.newplayer.data.StreamSelection 24 | import net.newpipe.newplayer.NewPlayer 25 | 26 | sealed interface StreamExceptionResponse 27 | 28 | /*** 29 | * Perform a specific action, like halting the playback or etc. 30 | */ 31 | data class ActionResponse(val action: () -> Unit) : StreamExceptionResponse 32 | 33 | /** 34 | * Tell NewPlayer with which stream selection of the current item the currently failing stream 35 | * selection should be replaced with. This can be used if only individual streams or tracks 36 | * corresponding to one item fail. 37 | * 38 | * If you want to reload the current item due to a timeout issue, or you want to replace the current 39 | * item all together, you should instead respond with [ReplaceItemResponse]. 40 | */ 41 | data class ReplaceStreamSelectionResponse(val streamSelection: StreamSelection) : 42 | StreamExceptionResponse 43 | 44 | /** 45 | * Tell NewPlayer with which item the currently failing item should be replaced with. 46 | * NewPlayer will continue to playback the new item at the position the last item failed. 47 | * The mew item will also be inserted in the same playlist position as the failing item was. 48 | * 49 | * This can also be used to just reload the currently failing item. Just supply the same item as 50 | * the currently failing item. NewPlayer will call the repository and ask for new streams for that 51 | * item. This can be used if stream urls get deprecated due to a timeout. 52 | * 53 | * If you don't want the whole item to be replaced/reloaded you should instead respond with a 54 | * [ReplaceStreamSelectionResponse]. 55 | */ 56 | data class ReplaceItemResponse(val newItem: String) : StreamExceptionResponse 57 | 58 | /** 59 | * Don't do anything to recover from the fail and forward the exception to [NewPlayer.errorFlow] 60 | */ 61 | class NoResponse : StreamExceptionResponse 62 | -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/repository/DelayTestRepository.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.repository 22 | 23 | import android.graphics.Bitmap 24 | import androidx.media3.common.MediaMetadata 25 | import androidx.media3.datasource.HttpDataSource 26 | import kotlinx.coroutines.delay 27 | import net.newpipe.newplayer.data.Chapter 28 | import net.newpipe.newplayer.data.Stream 29 | import net.newpipe.newplayer.data.Subtitle 30 | 31 | /** 32 | * This is a meta repository implementation meant for testing. You can give it an actual repository 33 | * and a delay, each request to your actual repository will then be delayed. This can be used to 34 | * test a slow network. 35 | * 36 | * @param actualRepo your actual MediaRepository 37 | * @param delayInMS is the delay in milliseconds 38 | */ 39 | class DelayTestRepository(val actualRepo: MediaRepository, var delayInMS: Long) : MediaRepository { 40 | override fun getRepoInfo() = actualRepo.getRepoInfo() 41 | 42 | override suspend fun getMetaInfo(item: String): MediaMetadata { 43 | delay(delayInMS) 44 | return actualRepo.getMetaInfo(item) 45 | } 46 | 47 | override suspend fun getStreams(item: String): List { 48 | delay(delayInMS) 49 | return actualRepo.getStreams(item) 50 | } 51 | 52 | override suspend fun getSubtitles(item: String): List { 53 | delay(delayInMS) 54 | return actualRepo.getSubtitles(item) 55 | } 56 | 57 | override suspend fun getPreviewThumbnail(item: String, timestampInMs: Long): Bitmap? { 58 | delay(delayInMS) 59 | return actualRepo.getPreviewThumbnail(item, timestampInMs) 60 | } 61 | 62 | override suspend fun getPreviewThumbnailsInfo(item: String): MediaRepository.PreviewThumbnailsInfo { 63 | delay(delayInMS) 64 | return actualRepo.getPreviewThumbnailsInfo(item) 65 | } 66 | 67 | override suspend fun getChapters(item: String): List { 68 | delay(delayInMS) 69 | return actualRepo.getChapters(item) 70 | } 71 | 72 | override suspend fun getTimestampLink(item: String, timestampInSeconds: Long): String { 73 | delay(delayInMS) 74 | return actualRepo.getTimestampLink(item, timestampInSeconds) 75 | } 76 | 77 | override fun getHttpDataSourceFactory(item: String): HttpDataSource.Factory { 78 | return actualRepo.getHttpDataSourceFactory(item) 79 | } 80 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/repository/MultiRepository.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.repository 22 | 23 | import androidx.media3.datasource.HttpDataSource 24 | import net.newpipe.newplayer.data.NewPlayerException 25 | 26 | /** 27 | * @param key is concatenated to the beginning of an **item** that is issued to a [MultiRepository] 28 | * this is used to identify which actual repository should handle the **item** 29 | * @param repository the repository that should handle that key 30 | */ 31 | data class MultiRepoEntry(val key: String, val repository: MediaRepository) 32 | 33 | /** 34 | * This repository can be used to combine multiple MediaRepositories. 35 | * Be aware that the string identifying an **item** is extended by the **key** of the repository. 36 | * This key is specified by [MultiRepoEntry.key]. 37 | * The string identifying an **item** called `item` would become `key:item` where repo key and 38 | * item are separated by a `:` character. 39 | * You need to take care yourself to encode the item strings accordingly when you are using a 40 | * MediaRepository. 41 | */ 42 | class MultiRepository(val actualRepositories: List) : MediaRepository { 43 | 44 | val repos = run { 45 | val repos = HashMap() 46 | for (entry in actualRepositories) { 47 | repos[entry.key] = entry.repository 48 | } 49 | repos 50 | } 51 | 52 | private data class RepoSelection ( 53 | val repo: MediaRepository, 54 | val item: String 55 | ) 56 | 57 | private fun getActualRepoAndItem( 58 | item: String, 59 | ): RepoSelection { 60 | val decomposedItem = item.split(":") 61 | val repoId = decomposedItem[0] 62 | val repo = repos[repoId] ?: throw NewPlayerException( 63 | "Could find a MediaRepository matching the item $item. Its repo key was apparently: $repoId" 64 | ) 65 | return RepoSelection(repo, decomposedItem[1]) 66 | } 67 | 68 | override fun getRepoInfo(): MediaRepository.RepoMetaInfo { 69 | var pullsDataFromNetwork = false 70 | var handlesTimestampLinks = true 71 | for (entry in actualRepositories) { 72 | pullsDataFromNetwork = 73 | entry.repository.getRepoInfo().pullsDataFromNetwork || pullsDataFromNetwork 74 | handlesTimestampLinks = 75 | entry.repository.getRepoInfo().canHandleTimestampedLinks && handlesTimestampLinks 76 | } 77 | 78 | return MediaRepository.RepoMetaInfo( 79 | pullsDataFromNetwork = pullsDataFromNetwork, 80 | canHandleTimestampedLinks = handlesTimestampLinks 81 | ) 82 | } 83 | 84 | override fun getHttpDataSourceFactory(item: String) = getActualRepoAndItem(item).let { 85 | it.repo.getHttpDataSourceFactory(it.item) 86 | } 87 | 88 | override suspend fun getMetaInfo(item: String) = getActualRepoAndItem(item).let { 89 | it.repo.getMetaInfo(it.item) 90 | } 91 | 92 | override suspend fun getStreams(item: String) = getActualRepoAndItem(item).let { 93 | it.repo.getStreams(it.item) 94 | } 95 | 96 | override suspend fun getSubtitles(item: String) = getActualRepoAndItem(item).let { 97 | it.repo.getSubtitles(it.item) 98 | } 99 | 100 | 101 | override suspend fun getPreviewThumbnail(item: String, timestampInMs: Long) = 102 | getActualRepoAndItem(item).let { 103 | it.repo.getPreviewThumbnail(it.item, timestampInMs) 104 | } 105 | 106 | override suspend fun getPreviewThumbnailsInfo(item: String) = 107 | getActualRepoAndItem(item).let { 108 | it.repo.getPreviewThumbnailsInfo(it.item) 109 | } 110 | 111 | override suspend fun getChapters(item: String) = 112 | getActualRepoAndItem(item).let { 113 | it.repo.getChapters(it.item) 114 | } 115 | 116 | override suspend fun getTimestampLink(item: String, timestampInSeconds: Long) = 117 | getActualRepoAndItem(item).let { 118 | it.repo.getTimestampLink(it.item, timestampInSeconds) 119 | } 120 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/repository/PlaceHolderRepository.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.repository 22 | 23 | import androidx.media3.common.MediaMetadata 24 | import net.newpipe.newplayer.data.Chapter 25 | import net.newpipe.newplayer.data.Stream 26 | import net.newpipe.newplayer.data.Subtitle 27 | 28 | /** 29 | * This is a simple placeholder repository that will return no information. It can be used 30 | * during development to be able to already lay out UI elements or setup and text compile 31 | * NewPlayer without actually having a functioning media repository. 32 | */ 33 | class PlaceHolderRepository : MediaRepository { 34 | override fun getRepoInfo() = 35 | MediaRepository.RepoMetaInfo(canHandleTimestampedLinks = true, pullsDataFromNetwork = false) 36 | 37 | override suspend fun getMetaInfo(item: String) = MediaMetadata.Builder().build() 38 | 39 | override suspend fun getStreams(item: String) = emptyList() 40 | 41 | override suspend fun getSubtitles(item: String) = emptyList() 42 | 43 | override suspend fun getPreviewThumbnail(item: String, timestampInMs: Long) = null 44 | 45 | override suspend fun getPreviewThumbnailsInfo(item: String) = 46 | MediaRepository.PreviewThumbnailsInfo(0, 0) 47 | 48 | override suspend fun getChapters(item: String) = emptyList() 49 | 50 | override suspend fun getTimestampLink(item: String, timestampInSeconds: Long) = "" 51 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/service/MediaNotification.kt: -------------------------------------------------------------------------------- 1 | package net.newpipe.newplayer.service 2 | 3 | import android.app.Activity 4 | import android.app.Notification 5 | import android.app.NotificationChannel 6 | import android.app.NotificationManager 7 | import android.app.PendingIntent 8 | import android.content.Intent 9 | import android.os.Build 10 | import androidx.annotation.OptIn 11 | import androidx.core.app.NotificationCompat 12 | import androidx.core.graphics.drawable.IconCompat 13 | import androidx.media3.common.util.UnstableApi 14 | import androidx.media3.session.MediaSession 15 | import androidx.media3.session.MediaStyleNotificationHelper 16 | import net.newpipe.newplayer.R 17 | 18 | /** @hide */ 19 | internal const val NEW_PLAYER_MEDIA_NOTIFICATION_ID = 17480 20 | /** @hide */ 21 | internal const val NEW_PLAYER_MEDIA_NOTIFICATION_CHANNEL_NAME = "Player" 22 | 23 | /** @hide */ 24 | internal const val NEW_PLAYER_REQUEST_CODE_OPEN_ACTIVITY = 0 25 | 26 | 27 | @OptIn(UnstableApi::class) 28 | /** @hide */ 29 | internal fun createNewPlayerNotification( 30 | service: NewPlayerService, 31 | session: MediaSession, 32 | notificationManager: NotificationManager, 33 | notificationIcon: IconCompat, 34 | playerActivity: Class 35 | ): Notification { 36 | 37 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 38 | notificationManager.createNotificationChannel( 39 | NotificationChannel( 40 | NEW_PLAYER_MEDIA_NOTIFICATION_CHANNEL_NAME, 41 | NEW_PLAYER_MEDIA_NOTIFICATION_CHANNEL_NAME, 42 | NotificationManager.IMPORTANCE_LOW 43 | ) 44 | ) 45 | } 46 | 47 | 48 | val onNotificationClickIntent = Intent(service, playerActivity).apply { 49 | addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) 50 | } 51 | val onNotificationClickPendingIntent = 52 | PendingIntent.getActivity( 53 | service, 54 | NEW_PLAYER_REQUEST_CODE_OPEN_ACTIVITY, 55 | onNotificationClickIntent, 56 | PendingIntent.FLAG_IMMUTABLE 57 | ) 58 | 59 | val notificationBuilder = 60 | NotificationCompat.Builder(service, NEW_PLAYER_MEDIA_NOTIFICATION_CHANNEL_NAME) 61 | .setContentTitle(service.resources.getString(R.string.new_player_name)) 62 | .setContentText(service.resources.getString(R.string.playing_in_background)) 63 | .setStyle(MediaStyleNotificationHelper.MediaStyle(session)) 64 | .setContentIntent(onNotificationClickPendingIntent) 65 | 66 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 67 | notificationBuilder.setSmallIcon(notificationIcon) 68 | } else { 69 | notificationBuilder 70 | .setSmallIcon(R.drawable.new_player_tiny_icon) 71 | } 72 | 73 | return notificationBuilder.build() 74 | } 75 | -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/service/NewPlayerNotificationCustomCommands.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | * 20 | */ 21 | 22 | package net.newpipe.newplayer.service 23 | 24 | import android.content.Context 25 | import android.os.Bundle 26 | import androidx.media3.session.CommandButton 27 | import androidx.media3.session.SessionCommand 28 | import net.newpipe.newplayer.R 29 | import net.newpipe.newplayer.NewPlayerImpl 30 | 31 | 32 | /** @hide */ 33 | internal data class CustomCommand( 34 | val action: String, 35 | val commandButton: CommandButton 36 | ) { 37 | companion object { 38 | const val NEW_PLAYER_NOTIFICATION_COMMAND_CLOSE_PLAYBACK = "NEW_PLAYER_CLOSE_PLAYBACK" 39 | } 40 | } 41 | 42 | /** 43 | * There could be more custom commands, other then just "Close". 44 | * In order to match NewPipe's current implementation there could also be a mechanism for 45 | * the [NewPlayerImpl] object that allows you to define which commands should be visible. 46 | * This way the user can decide which additional commands should be shown. 47 | * 48 | * @hide 49 | */ 50 | /** @hide */ 51 | internal fun buildCustomCommandList(context: Context) = listOf( 52 | CustomCommand( 53 | CustomCommand.NEW_PLAYER_NOTIFICATION_COMMAND_CLOSE_PLAYBACK, 54 | CommandButton.Builder() 55 | .setDisplayName(context.getString(R.string.close)) 56 | .setDisplayName("Close") 57 | .setSessionCommand( 58 | SessionCommand( 59 | CustomCommand.NEW_PLAYER_NOTIFICATION_COMMAND_CLOSE_PLAYBACK, 60 | Bundle() 61 | ) 62 | ) 63 | .setIconResId(R.drawable.close_24px) 64 | .build() 65 | ) 66 | ) 67 | 68 | -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/ui/ContentScale.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.ui 22 | 23 | /** 24 | * Depicts how the video should be layout in fullscreen mode. 25 | */ 26 | enum class ContentScale { 27 | /** 28 | * The video will fill the entire screen but it will also be stretched. 29 | */ 30 | STRETCHED, 31 | 32 | /** 33 | * The video will fit fully inside the screen's view pod, and will align with at least two 34 | * opposing borders. 35 | */ 36 | FIT_INSIDE, 37 | 38 | /** 39 | * The video will fill the entire screen. The aspect ratio of the video will remain true, 40 | * but parts of the video will be cropped of. However, the video will align with at least 41 | * two opposing borders, so that as little as possible video content is cropped of. 42 | */ 43 | CROP 44 | } 45 | -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/ui/LoadingPlaceholder.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | 22 | 23 | package net.newpipe.newplayer.ui 24 | 25 | import androidx.compose.foundation.layout.Box 26 | import androidx.compose.foundation.layout.aspectRatio 27 | import androidx.compose.foundation.layout.fillMaxSize 28 | import androidx.compose.material3.CircularProgressIndicator 29 | import androidx.compose.material3.Surface 30 | import androidx.compose.runtime.Composable 31 | import androidx.compose.ui.graphics.Color 32 | import androidx.compose.foundation.layout.fillMaxWidth 33 | import androidx.compose.foundation.layout.height 34 | import androidx.compose.foundation.layout.width 35 | import androidx.compose.material3.MaterialTheme 36 | import androidx.compose.ui.Alignment 37 | import androidx.compose.ui.Modifier 38 | import androidx.compose.ui.tooling.preview.Preview 39 | import androidx.compose.ui.unit.dp 40 | import net.newpipe.newplayer.ui.theme.VideoPlayerTheme 41 | 42 | @Composable 43 | 44 | /** @hide */ 45 | internal fun LoadingPlaceholder(aspectRatio: Float = 3F / 1F) { 46 | Surface( 47 | modifier = Modifier 48 | .fillMaxWidth() 49 | .aspectRatio(aspectRatio), 50 | color = Color.Black 51 | ) { 52 | Box(modifier = Modifier.fillMaxSize()) { 53 | CircularProgressIndicator( 54 | modifier = Modifier 55 | .width(64.dp) 56 | .height(64.dp) 57 | .align(Alignment.Center), 58 | color = MaterialTheme.colorScheme.onSurface 59 | ) 60 | } 61 | } 62 | } 63 | 64 | @Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape") 65 | @Composable 66 | private fun VideoPlayerLoaidingPlaceholderPreview() { 67 | VideoPlayerTheme { 68 | LoadingPlaceholder() 69 | } 70 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/ui/NewPlayerView.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.ui 22 | 23 | import android.content.Context 24 | import android.util.AttributeSet 25 | import android.view.LayoutInflater 26 | import android.widget.FrameLayout 27 | import androidx.compose.ui.platform.ComposeView 28 | import androidx.compose.ui.platform.ViewCompositionStrategy 29 | import dagger.hilt.android.AndroidEntryPoint 30 | import net.newpipe.newplayer.R 31 | import net.newpipe.newplayer.uiModel.InternalNewPlayerViewModel 32 | import net.newpipe.newplayer.uiModel.NewPlayerViewModel 33 | import net.newpipe.newplayer.ui.theme.VideoPlayerTheme 34 | import net.newpipe.newplayer.data.NewPlayerException 35 | 36 | 37 | /** 38 | * A wrapper for [NewPlayerUI] to allow NewPlayer to be used in a [views](https://developer.android.com/develop/ui/views/layout/declaring-layout) environment. 39 | */ 40 | @AndroidEntryPoint 41 | class NewPlayerView : FrameLayout { 42 | 43 | var viewModel: NewPlayerViewModel? = null 44 | set(value) { 45 | assert(viewModel is InternalNewPlayerViewModel?) { 46 | throw NewPlayerException("The view model given to NewPlayerView must be of type InternalNewPlayerViewModel. This can not be implemented externally, so do not extend NewPlayerViewModel") 47 | } 48 | field = value 49 | applyViewModel() 50 | } 51 | 52 | private val composeView:ComposeView 53 | 54 | @JvmOverloads 55 | constructor( 56 | context: Context, 57 | attrs: AttributeSet? = null, 58 | defStyleAttr: Int = 0 59 | ) : super(context, attrs, defStyleAttr) { 60 | val view = LayoutInflater.from(context).inflate(R.layout.video_player_view, this) 61 | composeView = view.findViewById(R.id.video_player_compose_view) 62 | 63 | applyViewModel() 64 | } 65 | 66 | private fun applyViewModel() { 67 | composeView.apply { 68 | setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) 69 | setContent { 70 | VideoPlayerTheme { 71 | NewPlayerUI(viewModel = viewModel as InternalNewPlayerViewModel?) 72 | } 73 | } 74 | } 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/ui/audioplayer/CoverArtUI.kt: -------------------------------------------------------------------------------- 1 | package net.newpipe.newplayer.ui.audioplayer 2 | 3 | import androidx.annotation.OptIn 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.material3.Card 7 | import androidx.compose.material3.CardDefaults 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.res.stringResource 11 | import androidx.compose.ui.unit.dp 12 | import androidx.media3.common.util.UnstableApi 13 | import net.newpipe.newplayer.R 14 | import net.newpipe.newplayer.ui.common.Thumbnail 15 | import net.newpipe.newplayer.uiModel.NewPlayerUIState 16 | 17 | /**hide*/ 18 | @OptIn(UnstableApi::class) 19 | @Composable 20 | internal fun CoverArtUI(modifier: Modifier = Modifier, uiState: NewPlayerUIState) { 21 | Box(modifier = modifier) { 22 | Card( 23 | elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) 24 | ) { 25 | Thumbnail( 26 | modifier = Modifier.fillMaxWidth(), 27 | thumbnail = uiState.currentlyPlaying?.mediaMetadata?.artworkUri, 28 | contentDescription = stringResource( 29 | id = R.string.stream_thumbnail 30 | ), 31 | ) 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/ui/audioplayer/ProgressUi.kt: -------------------------------------------------------------------------------- 1 | package net.newpipe.newplayer.ui.audioplayer 2 | 3 | import androidx.annotation.OptIn 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.offset 10 | import androidx.compose.foundation.layout.wrapContentHeight 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.tooling.preview.Preview 16 | import androidx.compose.ui.unit.dp 17 | import androidx.media3.common.util.UnstableApi 18 | import net.newpipe.newplayer.ui.common.NewPlayerSeeker 19 | import net.newpipe.newplayer.ui.common.ThumbPreview 20 | import net.newpipe.newplayer.ui.common.getLocale 21 | import net.newpipe.newplayer.ui.common.getTimeStringFromMs 22 | import net.newpipe.newplayer.ui.seeker.SeekerDefaults 23 | import net.newpipe.newplayer.ui.theme.VideoPlayerTheme 24 | import net.newpipe.newplayer.uiModel.InternalNewPlayerViewModel 25 | import net.newpipe.newplayer.uiModel.NewPlayerUIState 26 | import net.newpipe.newplayer.uiModel.NewPlayerViewModelDummy 27 | 28 | /**hide*/ 29 | @OptIn(UnstableApi::class) 30 | @Composable 31 | internal fun ProgressUI( 32 | modifier: Modifier = Modifier, 33 | viewModel: InternalNewPlayerViewModel, 34 | uiState: NewPlayerUIState 35 | ) { 36 | val locale = getLocale()!! 37 | 38 | Column(modifier = modifier) { 39 | Box( 40 | modifier = Modifier 41 | .height(0.dp) 42 | .fillMaxWidth() 43 | .wrapContentHeight(unbounded = true, align = Alignment.Bottom) 44 | ) { 45 | ThumbPreview( 46 | modifier = Modifier.offset(y = (-20).dp) /* We have this offset to make space for your thumb */, 47 | uiState = uiState, 48 | thumbSize = SeekerDefaults.ThumbRadius * 2, 49 | previewHeight = 120.dp 50 | ) 51 | } 52 | 53 | NewPlayerSeeker(viewModel = viewModel, uiState = uiState) 54 | Row { 55 | Text( 56 | getTimeStringFromMs( 57 | uiState.playbackPositionInMs, 58 | getLocale() ?: locale 59 | ) 60 | ) 61 | Box( 62 | modifier = Modifier 63 | .fillMaxWidth() 64 | .weight(1f) 65 | ) 66 | Text( 67 | getTimeStringFromMs( 68 | uiState.durationInMs, 69 | getLocale() ?: locale 70 | ) 71 | ) 72 | } 73 | } 74 | } 75 | 76 | @androidx.annotation.OptIn(UnstableApi::class) 77 | @Preview(device = "id:pixel_6") 78 | @Composable 79 | private fun AudioPlayerProgressUIPreview() { 80 | VideoPlayerTheme { 81 | ProgressUI( 82 | viewModel = NewPlayerViewModelDummy(), 83 | uiState = NewPlayerUIState.DUMMY.copy(playList = emptyList(), isLoading = false) 84 | ) 85 | } 86 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/ui/audioplayer/TitleView.kt: -------------------------------------------------------------------------------- 1 | package net.newpipe.newplayer.ui.audioplayer 2 | 3 | import androidx.annotation.OptIn 4 | import androidx.compose.foundation.basicMarquee 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.unit.dp 13 | import androidx.media3.common.util.UnstableApi 14 | import net.newpipe.newplayer.uiModel.NewPlayerUIState 15 | 16 | 17 | /**hide*/ 18 | @OptIn(UnstableApi::class) 19 | @Composable 20 | internal fun TitleView(modifier: Modifier = Modifier, uiState: NewPlayerUIState) { 21 | Column(modifier = modifier) { 22 | Text( 23 | text = uiState.currentlyPlaying?.mediaMetadata?.title.toString(), 24 | maxLines = 1, 25 | style = MaterialTheme.typography.headlineMedium, 26 | modifier = Modifier.basicMarquee(), 27 | ) 28 | Spacer(modifier = Modifier.height(6.dp)) 29 | Text( 30 | text = uiState.currentlyPlaying?.mediaMetadata?.artist.toString(), 31 | maxLines = 1, 32 | style = MaterialTheme.typography.bodyLarge, 33 | modifier = Modifier.basicMarquee(), 34 | ) 35 | } 36 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/ui/common/LanguageMenu.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.ui.common 22 | 23 | import androidx.annotation.OptIn 24 | import androidx.compose.material.icons.Icons 25 | import androidx.compose.material.icons.filled.Translate 26 | import androidx.compose.material3.DropdownMenu 27 | import androidx.compose.material3.DropdownMenuItem 28 | import androidx.compose.material3.Icon 29 | import androidx.compose.material3.Text 30 | import androidx.compose.runtime.Composable 31 | import androidx.compose.ui.platform.LocalContext 32 | import androidx.compose.ui.res.stringResource 33 | import androidx.media3.common.util.UnstableApi 34 | import net.newpipe.newplayer.R 35 | import net.newpipe.newplayer.logic.TrackUtils 36 | import net.newpipe.newplayer.uiModel.InternalNewPlayerViewModel 37 | import net.newpipe.newplayer.uiModel.NewPlayerUIState 38 | import net.newpipe.newplayer.uiModel.UIModeState 39 | import java.util.Locale 40 | 41 | @OptIn(UnstableApi::class) 42 | @Composable 43 | 44 | /** @hide */ 45 | internal fun LanguageMenu(uiState: NewPlayerUIState, viewModel: InternalNewPlayerViewModel, isVisible: Boolean, makeInvisible: () -> Unit) { 46 | val availableLanguages = TrackUtils.getAvailableLanguages(uiState.currentlyAvailableTracks) 47 | 48 | DropdownMenu(expanded = isVisible, onDismissRequest = { 49 | makeInvisible() 50 | viewModel.dialogVisible(false) 51 | }) { 52 | for (language in availableLanguages) { 53 | val locale = Locale(language) 54 | val context = LocalContext.current 55 | 56 | DropdownMenuItem( 57 | text = { 58 | Text(locale.displayLanguage) 59 | }, 60 | onClick = { /*TODO*/ 61 | showNotYetImplementedToast(context) 62 | makeInvisible() 63 | viewModel.dialogVisible(false) 64 | }) 65 | } 66 | } 67 | } 68 | 69 | 70 | @OptIn(UnstableApi::class) 71 | @Composable 72 | 73 | /** @hide */ 74 | internal fun LanguageMenuItem(uiState: NewPlayerUIState, onClick: () -> Unit) { 75 | val availableLanguages = TrackUtils.getAvailableLanguages(uiState.currentlyAvailableTracks) 76 | 77 | if (2 <= availableLanguages.size) { 78 | val language = TrackUtils.getAvailableLanguages(uiState.currentlyAvailableTracks)[0] 79 | val locale = Locale(language) 80 | DropdownMenuItem(text = { Text(locale.displayLanguage) }, 81 | leadingIcon = { 82 | Icon( 83 | imageVector = Icons.Filled.Translate, 84 | contentDescription = String.format( 85 | stringResource(R.string.menu_selected_language_item), 86 | locale.displayLanguage 87 | ) 88 | ) 89 | }, onClick = onClick) 90 | } 91 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/ui/common/NewPlayerSeeker.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | 22 | 23 | package net.newpipe.newplayer.ui.common 24 | 25 | import android.util.Log 26 | import androidx.annotation.OptIn 27 | import androidx.compose.material3.MaterialTheme 28 | import androidx.compose.runtime.Composable 29 | import androidx.compose.ui.Modifier 30 | import androidx.media3.common.util.UnstableApi 31 | import net.newpipe.newplayer.data.Chapter 32 | import net.newpipe.newplayer.uiModel.NewPlayerUIState 33 | import net.newpipe.newplayer.uiModel.InternalNewPlayerViewModel 34 | import net.newpipe.newplayer.ui.seeker.ChapterSegment 35 | import net.newpipe.newplayer.ui.seeker.DefaultSeekerColor 36 | import net.newpipe.newplayer.ui.seeker.Seeker 37 | import net.newpipe.newplayer.ui.seeker.SeekerColors 38 | 39 | private const val TAG = "NewPlayerSeeker" 40 | 41 | @OptIn(UnstableApi::class) 42 | @Composable 43 | 44 | /** @hide */ 45 | internal fun NewPlayerSeeker( 46 | modifier: Modifier = Modifier, 47 | viewModel: InternalNewPlayerViewModel, 48 | uiState: NewPlayerUIState 49 | ) { 50 | Seeker( 51 | modifier = modifier, 52 | value = uiState.seekerPosition, 53 | onValueChange = viewModel::seekPositionChanged, 54 | onValueChangeFinished = viewModel::seekingFinished, 55 | readAheadValue = uiState.bufferedPercentage, 56 | colors = customizedSeekerColors(), 57 | chapterSegments = getSeekerSegmentsFromChapters(uiState.chapters, uiState.durationInMs) 58 | ) 59 | } 60 | 61 | @Composable 62 | private fun customizedSeekerColors(): SeekerColors { 63 | val colors = DefaultSeekerColor( 64 | progressColor = MaterialTheme.colorScheme.primary, 65 | thumbColor = MaterialTheme.colorScheme.primary, 66 | trackColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f), 67 | readAheadColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f), 68 | disabledProgressColor = MaterialTheme.colorScheme.primary, 69 | disabledThumbColor = MaterialTheme.colorScheme.primary, 70 | disabledTrackColor = MaterialTheme.colorScheme.primary 71 | ) 72 | return colors 73 | } 74 | 75 | private fun getSeekerSegmentsFromChapters(chapters: List, duration: Long) = 76 | chapters 77 | .filter { chapter -> 78 | if (chapter.chapterStartInMs in 1.. 89 | val markPosition = chapter.chapterStartInMs.toFloat() / duration.toFloat() 90 | ChapterSegment(name = chapter.chapterTitle ?: "", start = markPosition) 91 | } 92 | 93 | -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/ui/common/NotYetImplementedToast.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.ui.common 22 | 23 | import android.content.Context 24 | import android.widget.Toast 25 | import net.newpipe.newplayer.R 26 | 27 | /** 28 | * This piece of code should eventually vanish once NewPlayer is sufficiently done ;-) 29 | */ 30 | 31 | /** @hide */ 32 | internal fun showNotYetImplementedToast(context: Context) { 33 | Toast.makeText( 34 | context, 35 | context.resources.getString(R.string.function_not_yet_implemented_toast), 36 | Toast.LENGTH_SHORT 37 | ).show() 38 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/ui/common/PlaylistControllButtons.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | 22 | package net.newpipe.newplayer.ui.common 23 | 24 | import androidx.annotation.OptIn 25 | import androidx.compose.material.icons.Icons 26 | import androidx.compose.material.icons.filled.Repeat 27 | import androidx.compose.material.icons.filled.RepeatOn 28 | import androidx.compose.material.icons.filled.RepeatOneOn 29 | import androidx.compose.material.icons.filled.Shuffle 30 | import androidx.compose.material.icons.filled.ShuffleOn 31 | import androidx.compose.material3.Icon 32 | import androidx.compose.material3.IconButton 33 | import androidx.compose.runtime.Composable 34 | import androidx.compose.ui.res.stringResource 35 | import androidx.media3.common.util.UnstableApi 36 | import net.newpipe.newplayer.R 37 | import net.newpipe.newplayer.data.RepeatMode 38 | 39 | import net.newpipe.newplayer.uiModel.NewPlayerUIState 40 | import net.newpipe.newplayer.uiModel.InternalNewPlayerViewModel 41 | 42 | @OptIn(UnstableApi::class) 43 | @Composable 44 | 45 | /** @hide */ 46 | internal fun RepeatModeButton(viewModel: InternalNewPlayerViewModel, uiState: NewPlayerUIState) { 47 | IconButton( 48 | onClick = viewModel::cycleRepeatMode 49 | ) { 50 | when (uiState.repeatMode) { 51 | RepeatMode.DO_NOT_REPEAT -> Icon( 52 | imageVector = Icons.Filled.Repeat, 53 | contentDescription = stringResource(R.string.repeat_mode_no_repeat) 54 | ) 55 | 56 | RepeatMode.REPEAT_ALL -> Icon( 57 | imageVector = Icons.Filled.RepeatOn, 58 | contentDescription = stringResource(R.string.repeat_mode_repeat_all) 59 | ) 60 | 61 | RepeatMode.REPEAT_ONE -> Icon( 62 | imageVector = Icons.Filled.RepeatOneOn, 63 | contentDescription = stringResource(R.string.repeat_mode_repeat_all) 64 | ) 65 | } 66 | } 67 | } 68 | 69 | @OptIn(UnstableApi::class) 70 | @Composable 71 | 72 | /** @hide */ 73 | internal fun ShuffleModeButton(viewModel: InternalNewPlayerViewModel, uiState: NewPlayerUIState) { 74 | IconButton( 75 | onClick = viewModel::toggleShuffle 76 | ) { 77 | if (uiState.shuffleEnabled) { 78 | Icon( 79 | imageVector = Icons.Filled.ShuffleOn, 80 | contentDescription = stringResource(R.string.shuffle_off) 81 | ) 82 | } else { 83 | Icon( 84 | imageVector = Icons.Filled.Shuffle, 85 | contentDescription = stringResource(R.string.shuffle_on) 86 | ) 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/ui/common/RememberHapticFeedback.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Calvin Liang 3 | * 4 | * @Author Calvin Liang 5 | * @Author Christian Schabesberger 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * 19 | */ 20 | 21 | package net.newpipe.newplayer.ui.common 22 | 23 | import android.os.Build 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.runtime.remember 26 | import androidx.compose.ui.platform.LocalView 27 | 28 | 29 | /** @hide */ 30 | internal enum class ReorderHapticFeedbackType { 31 | START, 32 | MOVE, 33 | END, 34 | } 35 | 36 | 37 | /** @hide */ 38 | internal open class ReorderHapticFeedback { 39 | open fun performHapticFeedback(type: ReorderHapticFeedbackType) { 40 | // no-op 41 | } 42 | } 43 | 44 | @Composable 45 | 46 | /** @hide */ 47 | internal fun rememberReorderHapticFeedback(): ReorderHapticFeedback { 48 | val view = LocalView.current 49 | 50 | val reorderHapticFeedback = remember { 51 | object : ReorderHapticFeedback() { 52 | override fun performHapticFeedback(type: ReorderHapticFeedbackType) { 53 | when (type) { 54 | ReorderHapticFeedbackType.START -> 55 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { 56 | view.performHapticFeedback(android.view.HapticFeedbackConstants.DRAG_START) 57 | } 58 | 59 | ReorderHapticFeedbackType.MOVE -> 60 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { 61 | view.performHapticFeedback(android.view.HapticFeedbackConstants.SEGMENT_FREQUENT_TICK) 62 | } 63 | 64 | ReorderHapticFeedbackType.END -> 65 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 66 | view.performHapticFeedback(android.view.HapticFeedbackConstants.GESTURE_END) 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | return reorderHapticFeedback 74 | } 75 | -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/ui/seeker/SeekerState.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Vivek Singh 3 | * 4 | * @Author Vivek Singh 5 | * @Author Christian Schabesberger 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * https://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * 19 | * Original code was taken from: https://github.com/2307vivek/Seeker/ 20 | * 21 | */ 22 | package net.newpipe.newplayer.ui.seeker 23 | 24 | import androidx.compose.foundation.gestures.DraggableState 25 | import androidx.compose.runtime.Composable 26 | import androidx.compose.runtime.Immutable 27 | import androidx.compose.runtime.Stable 28 | import androidx.compose.runtime.getValue 29 | import androidx.compose.runtime.mutableStateOf 30 | import androidx.compose.runtime.remember 31 | import androidx.compose.runtime.setValue 32 | import androidx.compose.ui.graphics.Color 33 | 34 | /** 35 | * A state object which can be hoisted to observe the current segment of Seeker. In most cases this 36 | * will be created by [rememberSeekerState] 37 | * */ 38 | @Stable 39 | 40 | /** @hide */ 41 | internal class SeekerState() { 42 | 43 | /** 44 | * The current segment corresponding to the current seeker value. 45 | * */ 46 | var currentSegment: Segment by mutableStateOf(Segment.Unspecified) 47 | 48 | 49 | /** @hide */ 50 | internal var onDrag: ((Float) -> Unit)? = null 51 | 52 | 53 | /** @hide */ 54 | internal val draggableState = DraggableState { 55 | onDrag?.invoke(it) 56 | } 57 | 58 | 59 | /** @hide */ 60 | internal fun currentSegment( 61 | value: Float, 62 | segments: List 63 | ) = (segments.findLast { value >= it.start } ?: Segment.Unspecified).also { this.currentSegment = it } 64 | } 65 | 66 | /** 67 | * Creates a SeekerState which will be remembered across compositions. 68 | * */ 69 | @Composable 70 | 71 | /** @hide */ 72 | internal fun rememberSeekerState(): SeekerState = remember { 73 | SeekerState() 74 | } 75 | 76 | /** 77 | * A class to hold information about a segment. 78 | * @param name name of the segment 79 | * @param start the value at which this segment should start in the track. This should must be in the 80 | * range of the Seeker range values. 81 | * @param color the color of the segment 82 | * */ 83 | @Immutable 84 | 85 | /** @hide */ 86 | internal data class Segment( 87 | val name: String, 88 | val start: Float, 89 | val end: Float, 90 | val color: Color = Color.Unspecified 91 | ) { 92 | companion object { 93 | val Unspecified = Segment(name = "", start = 0f, end = 0f) 94 | } 95 | } 96 | 97 | @Immutable 98 | 99 | /** @hide */ 100 | internal data class ChapterSegment( 101 | val name: String, 102 | val start: Float, 103 | val color: Color = Color.Unspecified 104 | ) { 105 | companion object { 106 | val Unspecified = ChapterSegment(name = "", start = 0f) 107 | } 108 | } 109 | 110 | @Immutable 111 | 112 | /** @hide */ 113 | internal data class SegmentPxs( 114 | val name: String, 115 | val startPx: Float, 116 | val endPx: Float, 117 | val color: Color 118 | ) -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/ui/seeker/SeekerUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Vivek Singh 3 | * 4 | * @Author Vivek Singh 5 | * @Author Christian Schabesberger 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * https://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * 19 | * Original code was taken from: https://github.com/2307vivek/Seeker/ 20 | * 21 | */ 22 | package net.newpipe.newplayer.ui.seeker 23 | 24 | // returns the corresponding position in pixels of progress in the the slider. 25 | 26 | /** @hide */ 27 | internal fun valueToPx( 28 | value: Float, 29 | widthPx: Float, 30 | range: ClosedFloatingPointRange 31 | ): Float { 32 | val rangeSIze = range.endInclusive - range.start 33 | val p = value.coerceIn(range.start, range.endInclusive) 34 | val progressPercent = (p - range.start) * 100 / rangeSIze 35 | return (progressPercent * widthPx / 100) 36 | } 37 | 38 | // returns the corresponding progress value for a position in slider 39 | 40 | /** @hide */ 41 | internal fun pxToValue( 42 | position: Float, 43 | widthPx: Float, 44 | range: ClosedFloatingPointRange 45 | ): Float { 46 | val rangeSize = range.endInclusive - range.start 47 | val percent = position * 100 / widthPx 48 | return ((percent * (rangeSize) / 100) + range.start).coerceIn( 49 | range.start, 50 | range.endInclusive 51 | ) 52 | } 53 | 54 | // converts the start value of a segment to the corresponding start and end pixel values 55 | // at which the segment will start and end on the track. 56 | 57 | /** @hide */ 58 | internal fun segmentToPxValues( 59 | segments: List, 60 | range: ClosedFloatingPointRange, 61 | widthPx: Float, 62 | ): List { 63 | 64 | val rangeSize = range.endInclusive - range.start 65 | val sortedSegments = segments.distinct().sortedBy { it.start } 66 | val segmentRangesPxs = sortedSegments.map { segment -> 67 | 68 | // percent of the start of this segment in the range size 69 | val percentStart = (segment.start - range.start) * 100 / rangeSize 70 | val percentEnd = (segment.end - range.start) * 100 / rangeSize 71 | val startPx = percentStart * widthPx / 100 72 | val endPx = percentEnd * widthPx / 100 73 | Pair(startPx, endPx) 74 | } 75 | 76 | return sortedSegments.mapIndexed { index, segment -> 77 | SegmentPxs( 78 | name = segment.name, 79 | color = segment.color, 80 | startPx = segmentRangesPxs[index].first, 81 | endPx = segmentRangesPxs[index].second 82 | ) 83 | } 84 | } 85 | 86 | 87 | /** @hide */ 88 | internal fun chapterSegmentToPxValues( 89 | segments: List, 90 | range: ClosedFloatingPointRange, 91 | widthPx: Float, 92 | ): List { 93 | 94 | val rangeSize = range.endInclusive - range.start 95 | val sortedSegments = segments.distinct().sortedBy { it.start } 96 | 97 | val segmentStartPxs = sortedSegments.map { segment -> 98 | 99 | // percent of the start of this segment in the range size 100 | val percent = (segment.start - range.start) * 100 / rangeSize 101 | val startPx = percent * widthPx / 100 102 | startPx 103 | } 104 | 105 | return sortedSegments.mapIndexed { index, segment -> 106 | val endPx = if (index != sortedSegments.lastIndex) segmentStartPxs[index + 1] else widthPx 107 | SegmentPxs( 108 | name = segment.name, 109 | color = segment.color, 110 | startPx = segmentStartPxs[index], 111 | endPx = endPx 112 | ) 113 | } 114 | } 115 | 116 | 117 | /** @hide */ 118 | internal fun rtlAware(value: Float, widthPx: Float, isRtl: Boolean) = 119 | if (isRtl) widthPx - value else value 120 | 121 | 122 | /** @hide */ 123 | internal fun lerp(start: Float, end: Float, fraction: Float): Float { 124 | return (1 - fraction) * start + fraction * end 125 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/ui/selection_ui/ChapterSelectTopBar.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | 22 | package net.newpipe.newplayer.ui.selection_ui 23 | 24 | import androidx.compose.foundation.layout.fillMaxSize 25 | import androidx.compose.material.icons.Icons 26 | import androidx.compose.material.icons.filled.Close 27 | import androidx.compose.material3.ExperimentalMaterial3Api 28 | import androidx.compose.material3.Icon 29 | import androidx.compose.material3.IconButton 30 | import androidx.compose.material3.Surface 31 | import androidx.compose.material3.Text 32 | import androidx.compose.material3.TopAppBar 33 | import androidx.compose.material3.TopAppBarDefaults.topAppBarColors 34 | import androidx.compose.runtime.Composable 35 | import androidx.compose.ui.Modifier 36 | import androidx.compose.ui.graphics.Color 37 | import androidx.compose.ui.res.stringResource 38 | import androidx.compose.ui.text.style.TextOverflow 39 | import androidx.compose.ui.tooling.preview.Preview 40 | import net.newpipe.newplayer.R 41 | import net.newpipe.newplayer.ui.theme.VideoPlayerTheme 42 | 43 | /** @hide */ 44 | @OptIn(ExperimentalMaterial3Api::class) 45 | @Composable 46 | internal fun ChapterSelectTopBar(modifier: Modifier = Modifier, onClose: () -> Unit) { 47 | TopAppBar(modifier = modifier, 48 | colors = topAppBarColors(containerColor = Color.Transparent), 49 | title = { 50 | Text(stringResource(R.string.chapter), maxLines = 1, overflow = TextOverflow.Ellipsis) 51 | }, actions = { 52 | IconButton( 53 | onClick = onClose 54 | ) { 55 | Icon( 56 | imageVector = Icons.Filled.Close, 57 | contentDescription = stringResource(R.string.close_chapter_selection) 58 | ) 59 | } 60 | }) 61 | } 62 | 63 | @Preview(device = "spec:width=1080px,height=150px,dpi=440,orientation=landscape") 64 | @Composable 65 | private fun ChapterTopBarPreview() { 66 | VideoPlayerTheme { 67 | Surface(modifier = Modifier.fillMaxSize(), color = Color.DarkGray) { 68 | ChapterSelectTopBar(modifier = Modifier.fillMaxSize()) {} 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/ui/selection_ui/ChapterSelectUI.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.ui.selection_ui 22 | 23 | import androidx.annotation.OptIn 24 | import androidx.compose.foundation.layout.Arrangement 25 | import androidx.compose.foundation.layout.Box 26 | import androidx.compose.foundation.layout.fillMaxSize 27 | import androidx.compose.foundation.layout.padding 28 | import androidx.compose.foundation.layout.windowInsetsPadding 29 | import androidx.compose.foundation.lazy.LazyColumn 30 | import androidx.compose.material3.MaterialTheme 31 | import androidx.compose.material3.Scaffold 32 | import androidx.compose.material3.Surface 33 | import androidx.compose.runtime.Composable 34 | import androidx.compose.ui.Modifier 35 | import androidx.compose.ui.graphics.Color 36 | import androidx.compose.ui.tooling.preview.Preview 37 | import androidx.compose.ui.unit.dp 38 | import androidx.media3.common.util.UnstableApi 39 | import net.newpipe.newplayer.uiModel.NewPlayerUIState 40 | import net.newpipe.newplayer.uiModel.InternalNewPlayerViewModel 41 | import net.newpipe.newplayer.uiModel.NewPlayerViewModelDummy 42 | import net.newpipe.newplayer.ui.theme.VideoPlayerTheme 43 | import net.newpipe.newplayer.ui.common.getEmbeddedUiConfig 44 | import net.newpipe.newplayer.ui.common.getInsets 45 | 46 | @OptIn(UnstableApi::class) 47 | @Composable 48 | 49 | /** @hide */ 50 | internal fun ChapterSelectUI( 51 | viewModel: InternalNewPlayerViewModel, 52 | uiState: NewPlayerUIState, 53 | shownInAudioPlayer: Boolean 54 | ) { 55 | val insets = getInsets() 56 | 57 | val embeddedUiConfig = getEmbeddedUiConfig() 58 | 59 | Scaffold( 60 | modifier = Modifier 61 | .fillMaxSize() 62 | .windowInsetsPadding(insets), 63 | containerColor = if (shownInAudioPlayer) 64 | MaterialTheme.colorScheme.background 65 | else 66 | Color.Transparent, 67 | topBar = { 68 | ChapterSelectTopBar( 69 | onClose = { 70 | viewModel.changeUiMode( 71 | uiState.uiMode.getNextModeWhenBackPressed() ?: uiState.uiMode, 72 | embeddedUiConfig 73 | ) 74 | } 75 | ) 76 | } 77 | ) { innerPadding -> 78 | Box(modifier = Modifier.padding(innerPadding)) { 79 | LazyColumn( 80 | modifier = Modifier 81 | .padding(start = 5.dp, end = 5.dp) 82 | .fillMaxSize(), 83 | verticalArrangement = Arrangement.spacedBy(5.dp), 84 | ) { 85 | items(uiState.chapters.size) { chapterIndex -> 86 | val chapter = uiState.chapters[chapterIndex] 87 | ChapterItem( 88 | id = chapterIndex, 89 | chapterTitle = chapter.chapterTitle ?: "", 90 | chapterStartInMs = chapter.chapterStartInMs, 91 | thumbnail = chapter.thumbnail, 92 | onClicked = { 93 | viewModel.chapterSelected(chapterIndex) 94 | }, 95 | isCurrentChapter = isActiveChapter( 96 | chapterIndex, 97 | uiState.chapters, 98 | uiState.playbackPositionInMs 99 | ) 100 | ) 101 | } 102 | } 103 | } 104 | } 105 | } 106 | 107 | 108 | @OptIn(UnstableApi::class) 109 | @Preview(device = "id:pixel_5") 110 | @Composable 111 | private fun VideoPlayerChannelSelectUIPreview() { 112 | VideoPlayerTheme { 113 | Surface(modifier = Modifier.fillMaxSize(), color = Color.DarkGray) { 114 | ChapterSelectUI( 115 | viewModel = NewPlayerViewModelDummy(), 116 | uiState = NewPlayerUIState.DUMMY, 117 | shownInAudioPlayer = false 118 | ) 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.ui.theme 22 | 23 | import androidx.compose.material3.MaterialTheme 24 | import androidx.compose.material3.darkColorScheme 25 | 26 | import androidx.compose.runtime.Composable 27 | 28 | val VideoPlayerColorScheme = darkColorScheme( 29 | primary = video_player_primary, 30 | onPrimary = video_player_onPrimary, 31 | primaryContainer = video_player_primaryContainer, 32 | onPrimaryContainer = video_player_onPrimaryContainer, 33 | secondary = video_player_secondary, 34 | onSecondary = video_player_onSecondary, 35 | secondaryContainer = video_player_secondaryContainer, 36 | onSecondaryContainer = video_player_onSecondaryContainer, 37 | tertiary = video_player_tertiary, 38 | onTertiary = video_player_onTertiary, 39 | tertiaryContainer = video_player_tertiaryContainer, 40 | onTertiaryContainer = video_player_onTertiaryContainer, 41 | error = video_player_error, 42 | errorContainer = video_player_errorContainer, 43 | onError = video_player_onError, 44 | onErrorContainer = video_player_onErrorContainer, 45 | background = video_player_background, 46 | onBackground = video_player_onBackground, 47 | surface = video_player_surface, 48 | onSurface = video_player_onSurface, 49 | surfaceVariant = video_player_surfaceVariant, 50 | onSurfaceVariant = video_player_onSurfaceVariant, 51 | outline = video_player_outline, 52 | inverseOnSurface = video_player_inverseOnSurface, 53 | inverseSurface = video_player_inverseSurface, 54 | inversePrimary = video_player_inversePrimary, 55 | surfaceTint = video_player_surfaceTint, 56 | outlineVariant = video_player_outlineVariant, 57 | scrim = video_player_scrim, 58 | ) 59 | 60 | @Composable 61 | /** @hide */ 62 | internal fun VideoPlayerTheme( 63 | content: @Composable () -> Unit 64 | ) { 65 | MaterialTheme( 66 | colorScheme = VideoPlayerColorScheme, 67 | typography = Typography, 68 | content = content 69 | ) 70 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.ui.theme 22 | 23 | import androidx.compose.material3.Typography 24 | import androidx.compose.ui.text.TextStyle 25 | import androidx.compose.ui.text.font.FontFamily 26 | import androidx.compose.ui.text.font.FontWeight 27 | import androidx.compose.ui.unit.sp 28 | 29 | // Set of Material typography styles to start with 30 | val Typography = Typography( 31 | bodyLarge = TextStyle( 32 | fontFamily = FontFamily.Default, 33 | fontWeight = FontWeight.Normal, 34 | fontSize = 16.sp, 35 | lineHeight = 24.sp, 36 | letterSpacing = 0.5.sp 37 | ) 38 | /* Other default text styles to override 39 | titleLarge = TextStyle( 40 | fontFamily = FontFamily.Default, 41 | fontWeight = FontWeight.Normal, 42 | fontSize = 22.sp, 43 | lineHeight = 28.sp, 44 | letterSpacing = 0.sp 45 | ), 46 | labelSmall = TextStyle( 47 | fontFamily = FontFamily.Default, 48 | fontWeight = FontWeight.Medium, 49 | fontSize = 11.sp, 50 | lineHeight = 16.sp, 51 | letterSpacing = 0.5.sp 52 | ) 53 | */ 54 | ) -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/GestureUI.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.ui.videoplayer 22 | 23 | import androidx.annotation.OptIn 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.graphics.Color 27 | import androidx.media3.common.util.UnstableApi 28 | import net.newpipe.newplayer.uiModel.NewPlayerUIState 29 | import net.newpipe.newplayer.uiModel.InternalNewPlayerViewModel 30 | import net.newpipe.newplayer.ui.videoplayer.gesture_ui.EmbeddedGestureUI 31 | import net.newpipe.newplayer.ui.videoplayer.gesture_ui.FullscreenGestureUI 32 | 33 | private const val TAG = "TouchUi" 34 | 35 | /** @hide */ 36 | internal val INDICATOR_BACKGROUND_COLOR = Color.Black.copy(alpha = 0.3f) 37 | 38 | @OptIn(UnstableApi::class) 39 | @Composable 40 | 41 | /** @hide */ 42 | internal fun GestureUI( 43 | modifier: Modifier, 44 | viewModel: InternalNewPlayerViewModel, 45 | uiState: NewPlayerUIState, 46 | onVolumeIndicatorVisibilityChanged: (Boolean) -> Unit 47 | ) { 48 | if (uiState.uiMode.fullscreen) { 49 | FullscreenGestureUI( 50 | modifier = modifier, 51 | viewModel = viewModel, 52 | uiState = uiState, 53 | onVolumeIndicatorVisibilityChanged = onVolumeIndicatorVisibilityChanged 54 | ) 55 | } else { 56 | EmbeddedGestureUI( 57 | modifier = modifier, viewModel = viewModel, uiState = uiState 58 | ) 59 | } 60 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/gesture_ui/FadedAnimationForSeekFeedback.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.ui.videoplayer.gesture_ui 22 | 23 | import androidx.compose.animation.AnimatedVisibility 24 | import androidx.compose.animation.core.tween 25 | import androidx.compose.animation.fadeIn 26 | import androidx.compose.animation.fadeOut 27 | import androidx.compose.runtime.Composable 28 | import androidx.compose.runtime.getValue 29 | import androidx.compose.runtime.mutableStateOf 30 | import androidx.compose.runtime.remember 31 | import androidx.compose.runtime.setValue 32 | 33 | /** @hide */ 34 | @Composable 35 | internal fun FadedAnimationForSeekFeedback( 36 | fastSeekSeconds: Int, 37 | backwards: Boolean = false, 38 | content: @Composable (fastSeekSecondsToDisplay:Int) -> Unit 39 | ) { 40 | 41 | var lastSecondsValue by remember { 42 | mutableStateOf(0) 43 | } 44 | 45 | val visible = if (backwards) { 46 | fastSeekSeconds < 0 47 | } else { 48 | 0 < fastSeekSeconds 49 | } 50 | 51 | val disappearImmediately = if (backwards) { 52 | 0 < fastSeekSeconds 53 | } else { 54 | fastSeekSeconds < 0 55 | } 56 | 57 | val valueToDisplay = if(visible) { 58 | lastSecondsValue = fastSeekSeconds 59 | fastSeekSeconds 60 | } else { 61 | lastSecondsValue 62 | } 63 | 64 | if (!disappearImmediately) { 65 | AnimatedVisibility( 66 | visible = visible, 67 | enter = fadeIn(animationSpec = tween(SEEK_ANIMATION_FADE_IN)), 68 | exit = fadeOut( 69 | animationSpec = tween(SEEK_ANIMATION_FADE_OUT) 70 | ) 71 | ) { 72 | content(valueToDisplay) 73 | } 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/gesture_ui/TouchedPosition.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.ui.videoplayer.gesture_ui 22 | 23 | 24 | /** @hide */ 25 | internal data class TouchedPosition(val x: Float, val y: Float) { 26 | operator fun minus(other: TouchedPosition) = TouchedPosition(this.x - other.x, this.y - other.y) 27 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/gesture_ui/VolumeCircle.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.ui.videoplayer.gesture_ui 22 | 23 | import android.util.Log 24 | import androidx.compose.foundation.Canvas 25 | import androidx.compose.foundation.layout.Box 26 | import androidx.compose.foundation.layout.size 27 | import androidx.compose.material.icons.Icons 28 | import androidx.compose.material.icons.automirrored.filled.VolumeDown 29 | import androidx.compose.material.icons.automirrored.filled.VolumeMute 30 | import androidx.compose.material.icons.automirrored.filled.VolumeOff 31 | import androidx.compose.material.icons.automirrored.filled.VolumeUp 32 | import androidx.compose.material.icons.filled.BrightnessHigh 33 | import androidx.compose.material.icons.filled.BrightnessLow 34 | import androidx.compose.material.icons.filled.BrightnessMedium 35 | import androidx.compose.material3.Icon 36 | import androidx.compose.material3.Surface 37 | import androidx.compose.ui.graphics.drawscope.Stroke 38 | import androidx.compose.runtime.Composable 39 | import androidx.compose.ui.Alignment 40 | import androidx.compose.ui.Modifier 41 | import androidx.compose.ui.geometry.Offset 42 | import androidx.compose.ui.geometry.Size 43 | import androidx.compose.ui.graphics.Color 44 | import androidx.compose.ui.graphics.StrokeCap 45 | import androidx.compose.ui.res.stringResource 46 | import androidx.compose.ui.tooling.preview.Preview 47 | import androidx.compose.ui.unit.dp 48 | import net.newpipe.newplayer.R 49 | import net.newpipe.newplayer.ui.theme.VideoPlayerTheme 50 | import net.newpipe.newplayer.ui.videoplayer.INDICATOR_BACKGROUND_COLOR 51 | 52 | private const val TAG = "VolumeCircle" 53 | 54 | private const val LINE_STROKE_WIDTH =6 55 | private const val CIRCLE_SIZE = 130 56 | 57 | @Composable 58 | 59 | /** @hide */ 60 | internal fun VolumeCircle( 61 | modifier: Modifier = Modifier, 62 | volumeFraction: Float, 63 | isBrightness: Boolean = false 64 | ) { 65 | assert(volumeFraction in 0f..1f) { 66 | Log.e(TAG, "Volume fraction must be in ragne [0;1]. It was $volumeFraction") 67 | } 68 | 69 | Box(modifier) { 70 | Canvas(Modifier.size(CIRCLE_SIZE.dp)) { 71 | val arcSize = (CIRCLE_SIZE - LINE_STROKE_WIDTH).dp.toPx(); 72 | drawCircle(color = INDICATOR_BACKGROUND_COLOR, radius = (CIRCLE_SIZE / 2).dp.toPx()) 73 | drawArc( 74 | topLeft = Offset( 75 | (LINE_STROKE_WIDTH / 2).dp.toPx(), (LINE_STROKE_WIDTH / 2).dp.toPx() 76 | ), 77 | size = Size(arcSize, arcSize), 78 | startAngle = -90f, 79 | sweepAngle = 360f * volumeFraction, 80 | useCenter = false, 81 | color = Color.White, 82 | style = Stroke(width = LINE_STROKE_WIDTH.dp.toPx(), cap = StrokeCap.Round) 83 | ) 84 | } 85 | 86 | Icon( 87 | modifier = Modifier 88 | .align(Alignment.Center) 89 | .size(80.dp), 90 | imageVector = (if (isBrightness) getBrightnessIcon(volumeFraction = volumeFraction) 91 | else getVolumeIcon(volumeFraction = volumeFraction)), 92 | contentDescription = stringResource( 93 | id = if (isBrightness) R.string.brightness_indicator 94 | else R.string.volume_indicator 95 | ) 96 | ) 97 | } 98 | } 99 | 100 | @Composable 101 | private fun getVolumeIcon(volumeFraction: Float) = 102 | if (volumeFraction == 0f) Icons.AutoMirrored.Filled.VolumeOff 103 | else if (volumeFraction < 0.3) Icons.AutoMirrored.Filled.VolumeMute 104 | else if (volumeFraction < 0.6) Icons.AutoMirrored.Filled.VolumeDown 105 | else Icons.AutoMirrored.Filled.VolumeUp 106 | 107 | @Composable 108 | private fun getBrightnessIcon(volumeFraction: Float) = 109 | if (volumeFraction < 0.3) Icons.Filled.BrightnessLow 110 | else if (volumeFraction < 0.6) Icons.Filled.BrightnessMedium 111 | else Icons.Filled.BrightnessHigh 112 | 113 | @Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape") 114 | @Composable 115 | private fun VolumeCirclePreview() { 116 | VideoPlayerTheme { 117 | Surface(color = Color.White) { 118 | VolumeCircle(volumeFraction = 0.3f, isBrightness = false) 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/pip/PipParams.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.ui.videoplayer.pip 22 | 23 | import android.app.PictureInPictureParams 24 | import android.graphics.Rect 25 | import android.os.Build 26 | import android.util.Rational 27 | import androidx.annotation.RequiresApi 28 | 29 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP) 30 | private fun convertFloatToRational(number: Float) : Rational{ 31 | // TODO: Testing, "Attention, please". Feel the tension soon as someone mentions me 32 | 33 | if(number.isNaN()) { 34 | return Rational.NaN 35 | } 36 | 37 | val bits = java.lang.Float.floatToIntBits(number) 38 | 39 | // bit pattern based on IEEE 754 binary32 used for Float in the Java context 40 | val sign = bits ushr 31 41 | val exponent = ((bits ushr 23) xor (sign shl 7)) - 127 42 | val fraction = bits shl 8 // bits are "reversed" but that's not a problem 43 | 44 | var a = 1 45 | var b = 1 46 | 47 | for (i in 30 downTo 8) { 48 | a = a * 2 + ((fraction ushr i) and 1) 49 | b *= 2 50 | } 51 | 52 | if (exponent > 0)a *= (1 shl exponent) 53 | else b *= (1 shl -exponent) 54 | 55 | if (sign == 1) 56 | a *= -1 57 | 58 | return Rational(a, b) 59 | } 60 | 61 | 62 | /** @hide */ 63 | internal fun getPipParams(aspectRatio: Float, sourceRectHint: Rect) = 64 | if (Build.VERSION_CODES.O <= Build.VERSION.SDK_INT) { 65 | PictureInPictureParams.Builder() 66 | .setAspectRatio(convertFloatToRational(aspectRatio)) 67 | .setSourceRectHint(sourceRectHint) 68 | .also { 69 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 70 | it.setSeamlessResizeEnabled(true) 71 | } 72 | } 73 | .build() 74 | } else { 75 | null 76 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/pip/SupportsPiP.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.ui.videoplayer.pip 22 | 23 | import android.content.Context 24 | import android.content.pm.PackageManager 25 | import android.os.Build 26 | 27 | 28 | /** @hide */ 29 | internal fun supportsPip(context: Context) = 30 | if(Build.VERSION_CODES.N <= Build.VERSION.SDK_INT) { 31 | val isSupported by lazy { 32 | val pm = context.packageManager 33 | pm.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) 34 | } 35 | isSupported 36 | } else { 37 | false 38 | } 39 | -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/uiModel/EmbeddedUiConfig.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.uiModel 22 | 23 | import android.content.pm.ActivityInfo 24 | import android.os.Parcelable 25 | import kotlinx.android.parcel.Parcelize 26 | import net.newpipe.newplayer.ui.NewPlayerUI 27 | 28 | /** 29 | * This helps to restore properties of the UI/SystemUI when returning from fullscreen mode. 30 | * [NewPlayerViewModelImpl] uses this to store the properties when switching to a fullscreen mode. 31 | * When returning from a fullscreen mode [NewPlayerViewModelImpl] will store the current 32 | * EmbeddedUiConfig in the [NewPlayerUIState]. When returning from fullscreen mode 33 | * [NewPlayerUI] will then restore that configuration. 34 | * 35 | * TODO: At least in theory. Brightnes and the systembar theme don't work (correctly) right now. 36 | * 37 | */ 38 | @Parcelize 39 | data class EmbeddedUiConfig( 40 | val systemBarInLightMode: Boolean, 41 | val brightness: Float, 42 | val screenOrientation: Int 43 | ) : Parcelable { 44 | companion object { 45 | val DUMMY = EmbeddedUiConfig( 46 | systemBarInLightMode = true, 47 | brightness = -1f, 48 | screenOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT 49 | ) 50 | } 51 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/uiModel/InternalNewPlayerViewModel.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.uiModel 22 | 23 | import android.os.Bundle 24 | import androidx.annotation.OptIn 25 | import androidx.media3.common.util.UnstableApi 26 | 27 | @OptIn(UnstableApi::class) 28 | 29 | /** @hide */ 30 | internal interface InternalNewPlayerViewModel : NewPlayerViewModel { 31 | var minContentRatio: Float 32 | var maxContentRatio: Float 33 | var deviceInPowerSaveMode: Boolean 34 | 35 | fun initUIState(instanceState: Bundle) 36 | fun play() 37 | fun pause() 38 | fun prevStream() 39 | fun nextStream() 40 | fun changeUiMode(newUiModeState: UIModeState, embeddedUiConfig: EmbeddedUiConfig?) 41 | fun onBackPressed() 42 | fun seekPositionChanged(newValue: Float) 43 | fun seekingFinished() 44 | fun embeddedDraggedDown(offset: Float) 45 | fun fastSeek(count: Int) 46 | fun finishFastSeek() 47 | fun brightnessChange(changeRate: Float, systemBrightness: Float) 48 | fun volumeChange(changeRate: Float) 49 | fun chapterSelected(chapterId: Int) 50 | fun streamSelected(streamId: Int) 51 | fun cycleRepeatMode() 52 | fun cycleContentFitMode() 53 | fun toggleShuffle() 54 | fun onStorePlaylist() 55 | fun movePlaylistItem(from: Int, to: Int) 56 | fun removePlaylistItem(uniqueId: Long) 57 | fun onStreamItemDragFinished() 58 | fun dialogVisible(visible: Boolean) 59 | fun doneEnteringPip() 60 | fun resetHideDelayTimer() 61 | fun onShowPlaylistInAudioPlayerToggle() 62 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/uiModel/NewPlayerViewModel.kt: -------------------------------------------------------------------------------- 1 | package net.newpipe.newplayer.uiModel 2 | 3 | import android.app.Activity 4 | import androidx.annotation.OptIn 5 | import androidx.media3.common.util.UnstableApi 6 | import kotlinx.coroutines.flow.SharedFlow 7 | import kotlinx.coroutines.flow.StateFlow 8 | import net.newpipe.newplayer.NewPlayer 9 | import net.newpipe.newplayer.ui.NewPlayerUI 10 | import net.newpipe.newplayer.ui.ContentScale 11 | import net.newpipe.newplayer.data.PlayMode.IDLE 12 | 13 | 14 | /** 15 | * The NewPlayerViewModel must live and be owned by an Activity context. 16 | */ 17 | @OptIn(UnstableApi::class) 18 | interface NewPlayerViewModel { 19 | 20 | /** 21 | * The current instance of [NewPlayer]. If set to null the [NewPlayerUI] instance 22 | * which the viewModel talks too will display the same as if [NewPlayer] was in [IDLE] mode. 23 | * 24 | * You can always add or remove the NewPlayer instance. 25 | */ 26 | var newPlayer: NewPlayer? 27 | 28 | /** 29 | * Depicts weather the picture of the video should be stretched, fit inside or be cropped. 30 | * You can set it yourself, but [NewPlayerUI] can also change it. 31 | * 32 | * You should store this value in the settings database when the ViewModel gets destroyed. 33 | * You should then reload this value if the ViewModel gets recreated. 34 | * This way you ensure that whatever the user desires to see it stays persistent upon 35 | * app restarts. 36 | */ 37 | var contentFitMode: ContentScale 38 | 39 | /** 40 | * This represents the state the UI is in. [NewPlayerUI] will basically render out that state. 41 | * You can make your UI also listen to changes to this state. This is especially helpful 42 | * for switching to or from fullscreen or to or from PiP mode. 43 | */ 44 | val uiState: StateFlow 45 | 46 | /** 47 | * If the user dragged down the embedded video or audio player. This callback will tell you 48 | * how far the user dragged down. Keep in mind that the user can also decide to drag the 49 | * [NewPlayerUI] up again. If the [NewPlayerUI] is not dragged up to its original position 50 | * you should take over control and maybe make the [NewPlayerUI] snap to the bottom of the 51 | * screen to make space for your UI. 52 | */ 53 | val embeddedPlayerDraggedDownBy: SharedFlow 54 | 55 | /** 56 | * [NewPlayerUI] will use some back press events for its own navigation purposes. 57 | * If the viewModel decides that a back press event should not be handled by itself. 58 | * It will forward the event to you through this callback. 59 | */ 60 | val onBackPressed: SharedFlow 61 | 62 | /** 63 | * If this is set the audio player will display the playlist instead of the thumbnail of the 64 | * current stream. 65 | * 66 | * You should store this value in the settings database when the ViewModel gets destroyed. 67 | * You should then reload this value if the ViewModel gets recreated. 68 | * This way you ensure that whatever the user desires to see it stays persistent upon 69 | * app restarts. 70 | */ 71 | var showPlaylistInAudioPlayer: Boolean 72 | 73 | /** 74 | * This is something you have to call in the Activity that should host the [NewPlayerUI]. 75 | * See the [example app's](https://github.com/TeamNewPipe/NewPlayer/blob/master/test-app/src/main/java/net/newpipe/newplayer/testapp/MainActivity.kt) main activity to find out how to use this. 76 | * 77 | * Long story short your activity should handle the `onPictureInPictureModeChanged` event. 78 | * You can do this by adding this code to your Activity's `onCreate()` function: 79 | * ``` 80 | * addOnPictureInPictureModeChangedListener { mode -> 81 | * newPlayerViewModel.onPictureInPictureModeChanged(mode.isInPictureInPictureMode) 82 | * } 83 | * ``` 84 | */ 85 | fun onPictureInPictureModeChanged(isPictureInPictureMode: Boolean) 86 | } -------------------------------------------------------------------------------- /new-player/src/main/java/net/newpipe/newplayer/uiModel/NewPlayerViewModelDummy.kt: -------------------------------------------------------------------------------- 1 | package net.newpipe.newplayer.uiModel 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | import androidx.media3.common.util.UnstableApi 6 | import kotlinx.coroutines.flow.MutableSharedFlow 7 | import kotlinx.coroutines.flow.MutableStateFlow 8 | import kotlinx.coroutines.flow.SharedFlow 9 | import kotlinx.coroutines.flow.asSharedFlow 10 | import net.newpipe.newplayer.NewPlayer 11 | import net.newpipe.newplayer.ui.ContentScale 12 | 13 | /** 14 | * A dummy/placeholder implementation of the [NewPlayerViewModel] 15 | */ 16 | @UnstableApi 17 | open class NewPlayerViewModelDummy : InternalNewPlayerViewModel { 18 | override var newPlayer: NewPlayer? = null 19 | override val uiState = MutableStateFlow(NewPlayerUIState.DEFAULT) 20 | override var minContentRatio = 4F / 3F 21 | override var maxContentRatio = 16F / 9F 22 | override var contentFitMode = ContentScale.FIT_INSIDE 23 | override val embeddedPlayerDraggedDownBy = MutableSharedFlow().asSharedFlow() 24 | override val onBackPressed: SharedFlow = MutableSharedFlow().asSharedFlow() 25 | override var deviceInPowerSaveMode: Boolean = false 26 | override var showPlaylistInAudioPlayer: Boolean = false 27 | 28 | override fun initUIState(instanceState: Bundle) { 29 | println("dummy impl") 30 | } 31 | 32 | override fun play() { 33 | println("dummy impl") 34 | } 35 | 36 | override fun onBackPressed() { 37 | println("dummy impl") 38 | } 39 | 40 | override fun seekPositionChanged(newValue: Float) { 41 | println("dymmy seekPositionChanged: newValue: ${newValue}") 42 | } 43 | 44 | override fun seekingFinished() { 45 | println("dummy impl") 46 | } 47 | 48 | override fun embeddedDraggedDown(offset: Float) { 49 | println("dymmy embeddedDraggedDown: offset: ${offset}") 50 | } 51 | 52 | override fun fastSeek(steps: Int) { 53 | println("dummy impl: steps: $steps") 54 | } 55 | 56 | override fun finishFastSeek() { 57 | println("dummy impl") 58 | } 59 | 60 | override fun brightnessChange(changeRate: Float, systemBrightness: Float) { 61 | println("dummy impl") 62 | } 63 | 64 | override fun volumeChange(changeRate: Float) { 65 | println("dummy impl") 66 | } 67 | 68 | override fun chapterSelected(chapterId: Int) { 69 | println("dummp impl chapter selected: $chapterId") 70 | } 71 | 72 | override fun streamSelected(streamId: Int) { 73 | println("dummy impl stream selected: $streamId") 74 | } 75 | 76 | override fun cycleRepeatMode() { 77 | println("dummy impl") 78 | } 79 | 80 | override fun cycleContentFitMode() { 81 | println("dummy impl") 82 | } 83 | 84 | override fun toggleShuffle() { 85 | println("dummy impl") 86 | } 87 | 88 | override fun onStorePlaylist() { 89 | println("dummy impl") 90 | } 91 | 92 | override fun movePlaylistItem(from: Int, to: Int) { 93 | println("dummy impl") 94 | } 95 | 96 | override fun removePlaylistItem(uniqueId: Long) { 97 | println("dummy impl delete uniqueId: $uniqueId") 98 | } 99 | 100 | override fun onStreamItemDragFinished() { 101 | println("dummy impl") 102 | } 103 | 104 | override fun dialogVisible(visible: Boolean) { 105 | println("dummy impl dialog visible: $visible") 106 | } 107 | 108 | override fun doneEnteringPip() { 109 | println("dummy impl") 110 | } 111 | 112 | override fun resetHideDelayTimer() { 113 | println("dummy reset hide delay timer") 114 | } 115 | 116 | override fun onShowPlaylistInAudioPlayerToggle() { 117 | println("dummy impl") 118 | } 119 | 120 | override fun onPictureInPictureModeChanged(isPictureInPictureMode: Boolean) { 121 | println("dummy impl isInPictureInPictureMode: $isPictureInPictureMode") 122 | } 123 | 124 | override fun pause() { 125 | println("dummy pause") 126 | } 127 | 128 | override fun prevStream() { 129 | println("dummy impl") 130 | } 131 | 132 | override fun nextStream() { 133 | println("dummy impl") 134 | } 135 | 136 | override fun changeUiMode(newUiModeState: UIModeState, embeddedUiConfig: EmbeddedUiConfig?) { 137 | println("dummy uiMode change: New UI Mode State: $newUiModeState") 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /new-player/src/main/res/drawable/close_24px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /new-player/src/main/res/drawable/ic_play_seek_triangle.xml: -------------------------------------------------------------------------------- 1 | 20 | 21 | 26 | 29 | 30 | -------------------------------------------------------------------------------- /new-player/src/main/res/drawable/new_player_tiny_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /new-player/src/main/res/drawable/tiny_placeholder.xml: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /new-player/src/main/res/layout/video_player_framgent.xml: -------------------------------------------------------------------------------- 1 | 20 | 21 | 25 | 26 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /new-player/src/main/res/layout/video_player_view.xml: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /new-player/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | NewPlayer 23 | NewPlayer Fullscreen 24 | Open in browser 25 | Share timestamp 26 | More settings 27 | Audio Mode 28 | Fit screen 29 | Crop video 30 | Stretch video 31 | Subtitles 32 | Selected language: %s 33 | Playback speed 34 | Previous stream 35 | Fast rewind 36 | Next stream 37 | Fast forward 38 | Play 39 | Pause 40 | Toggle fullscreen 41 | Chapter selection 42 | Playlist item selection 43 | Fast seeking backward by %d seconds. 44 | Fast seeking forward by %d seconds. 45 | Seconds 46 | Volume indicator 47 | Brightness indicator 48 | Close chapter selection 49 | Close stream selection 50 | Chapter 51 | Chapter thumbnail 52 | Stream item thumbnail 53 | Stream item drag handle 54 | Repeat mode: No repeat 55 | Repeat mode: Repeat all 56 | Repeat mode: Repeat currently playing 57 | Shuffle playlist enabled 58 | Shuffle playlist disabled 59 | Save current playlist 60 | Close 61 | Stream Thumbnail 62 | Switch to details view 63 | Switch to fullscreen video mode 64 | Picture in picture 65 | Playing in the background… 66 | Video seek preview thumbnaila 67 | No other stream tracks available. 68 | Loading 69 | Function is not yet implemented. 70 | -------------------------------------------------------------------------------- /new-player/src/test/java/net/newpipe/newplayer/repository/MockMediaRepository.kt: -------------------------------------------------------------------------------- 1 | package net.newpipe.newplayer.repository 2 | 3 | import android.net.Uri 4 | import androidx.media3.common.MediaMetadata 5 | import androidx.media3.datasource.DefaultHttpDataSource 6 | import androidx.media3.datasource.HttpDataSource 7 | import io.mockk.every 8 | import io.mockk.mockk 9 | import io.mockk.mockkStatic 10 | import net.newpipe.newplayer.data.Chapter 11 | import net.newpipe.newplayer.data.Stream 12 | import net.newpipe.newplayer.data.Subtitle 13 | 14 | /** 15 | * Simple MediaRepository with mock values 16 | */ 17 | class MockMediaRepository : MediaRepository { 18 | val uriMock: Uri 19 | 20 | init { 21 | mockkStatic(Uri::class) 22 | every { Uri.parse(any()) } returns mockk("Uri") 23 | uriMock = Uri.parse("test/uri") 24 | } 25 | 26 | override fun getRepoInfo() = MediaRepository.RepoMetaInfo(canHandleTimestampedLinks = true, pullsDataFromNetwork = true) 27 | 28 | override suspend fun getMetaInfo(item: String) = MediaMetadata.Builder().setTitle("Test title").build() 29 | 30 | override suspend fun getStreams(item: String) = listOf( 31 | Stream("item1", uriMock, emptyList()), 32 | Stream("item2", uriMock, emptyList()), 33 | Stream("item3", uriMock, emptyList()) 34 | ) 35 | 36 | override suspend fun getSubtitles(item: String) = listOf( 37 | Subtitle(uriMock, "subtitle1"), 38 | Subtitle(uriMock, "subtitle2"), 39 | Subtitle(uriMock, "subtitle3") 40 | ) 41 | 42 | override suspend fun getPreviewThumbnail(item: String, timestampInMs: Long) = null 43 | 44 | override suspend fun getPreviewThumbnailsInfo(item: String) = MediaRepository.PreviewThumbnailsInfo(10, 500) 45 | 46 | override suspend fun getChapters(item: String) = listOf( 47 | Chapter(0, "chapter1", null), 48 | Chapter(5000, "chapter2", null), 49 | Chapter(1000, "chapter3", null), 50 | ) 51 | 52 | override suspend fun getTimestampLink(item: String, timestampInSeconds: Long) = "test/link" 53 | 54 | override fun getHttpDataSourceFactory(item: String): HttpDataSource.Factory { 55 | val factory = DefaultHttpDataSource.Factory() 56 | factory.setUserAgent("TestUserAgent") 57 | return factory 58 | } 59 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google { 4 | content { 5 | includeGroupByRegex("com\\.android.*") 6 | includeGroupByRegex("com\\.google.*") 7 | includeGroupByRegex("androidx.*") 8 | } 9 | } 10 | mavenCentral() 11 | gradlePluginPortal() 12 | } 13 | } 14 | dependencyResolutionManagement { 15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 16 | repositories { 17 | google() 18 | mavenCentral() 19 | maven(url ="https://jitpack.io") 20 | } 21 | } 22 | 23 | rootProject.name = "NewPlayer" 24 | include(":test-app") 25 | include(":new-player") 26 | -------------------------------------------------------------------------------- /test-app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /test-app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | plugins { 22 | alias(libs.plugins.android.application) 23 | alias(libs.plugins.jetbrains.kotlin.android) 24 | alias(libs.plugins.kotlinAndroidKsp) 25 | alias(libs.plugins.androidHilt) 26 | alias(libs.plugins.kotlinParcelize) 27 | alias(libs.plugins.composeCompiler) 28 | } 29 | 30 | android { 31 | namespace = "net.newpipe.newplayer.testapp" 32 | compileSdk = 35 33 | 34 | viewBinding { 35 | enable = true 36 | } 37 | 38 | buildFeatures { 39 | compose = true 40 | } 41 | composeOptions { 42 | kotlinCompilerExtensionVersion = "1.5.1" 43 | } 44 | 45 | hilt { 46 | enableExperimentalClasspathAggregation = true 47 | } 48 | 49 | defaultConfig { 50 | applicationId = "net.newpipe.newplayer.testapp" 51 | minSdk = 21 52 | targetSdk = 35 53 | versionCode = 1 54 | versionName = "1.0" 55 | 56 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 57 | vectorDrawables { 58 | useSupportLibrary = true 59 | } 60 | } 61 | 62 | buildTypes { 63 | release { 64 | isMinifyEnabled = false 65 | proguardFiles( 66 | getDefaultProguardFile("proguard-android-optimize.txt"), 67 | "proguard-rules.pro" 68 | ) 69 | } 70 | } 71 | compileOptions { 72 | sourceCompatibility = JavaVersion.VERSION_1_8 73 | targetCompatibility = JavaVersion.VERSION_1_8 74 | } 75 | kotlinOptions { 76 | jvmTarget = "1.8" 77 | } 78 | packaging { 79 | resources { 80 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 81 | } 82 | } 83 | 84 | } 85 | 86 | dependencies { 87 | implementation(libs.androidx.core.ktx) 88 | implementation(libs.androidx.appcompat) 89 | implementation(libs.material) 90 | implementation(libs.androidx.activity) 91 | implementation(libs.androidx.constraintlayout) 92 | implementation(libs.androidx.activity.compose) 93 | implementation(libs.androidx.material3) 94 | implementation(libs.androidx.ui.tooling) 95 | implementation(libs.androidx.material.icons.extended.android) 96 | implementation(libs.androidx.media3.exoplayer) 97 | implementation(libs.androidx.media3.ui) 98 | implementation(libs.hilt.android) 99 | implementation(libs.androidx.lifecycle.viewmodel.compose) 100 | implementation(libs.androidx.foundation) 101 | implementation(libs.androidx.fragment.ktx) 102 | implementation(libs.androidx.lifecycle.runtime.ktx) 103 | implementation(platform(libs.androidx.compose.bom)) 104 | implementation(libs.androidx.ui) 105 | implementation(libs.androidx.ui.graphics) 106 | implementation(libs.androidx.ui.tooling.preview) 107 | implementation(libs.androidx.hilt.navigation.compose) 108 | implementation(libs.coil.compose) 109 | implementation(libs.okhttp.android) 110 | implementation(libs.androidx.lifecycle.runtime.ktx) 111 | 112 | 113 | // development impl 114 | implementation(project(":new-player")) 115 | //jitpack test 116 | //implementation(libs.newplayer) 117 | 118 | ksp(libs.hilt.android.compiler) 119 | ksp(libs.androidx.hilt.compiler) 120 | 121 | androidTestImplementation(platform(libs.androidx.compose.bom)) 122 | androidTestImplementation(libs.androidx.ui.test.junit4) 123 | androidTestImplementation(libs.androidx.junit) 124 | androidTestImplementation(libs.androidx.espresso.core) 125 | 126 | debugImplementation(libs.androidx.ui.tooling) 127 | debugImplementation(libs.androidx.ui.test.manifest) 128 | 129 | testImplementation(libs.junit) 130 | } 131 | -------------------------------------------------------------------------------- /test-app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /test-app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 18 | 19 | 26 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /test-app/src/main/java/net/newpipe/newplayer/testapp/NewPlayerApp.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.testapp 22 | 23 | import android.app.Application 24 | import android.util.Log 25 | import dagger.hilt.android.HiltAndroidApp 26 | import kotlinx.coroutines.CoroutineScope 27 | import kotlinx.coroutines.Dispatchers 28 | import kotlinx.coroutines.Job 29 | import kotlinx.coroutines.flow.collect 30 | import kotlinx.coroutines.flow.collectLatest 31 | import kotlinx.coroutines.launch 32 | import net.newpipe.newplayer.NewPlayer 33 | import javax.inject.Inject 34 | 35 | private const val TAG = "NewPlayerApp" 36 | 37 | @HiltAndroidApp 38 | class NewPlayerApp : Application() { 39 | val appScope = CoroutineScope(Dispatchers.Default + Job()) 40 | } 41 | 42 | 43 | -------------------------------------------------------------------------------- /test-app/src/main/java/net/newpipe/newplayer/testapp/NewPlayerComponent.kt: -------------------------------------------------------------------------------- 1 | /* NewPlayer 2 | * 3 | * @author Christian Schabesberger 4 | * 5 | * Copyright (C) NewPipe e.V. 2024 6 | * 7 | * NewPlayer is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * NewPlayer is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with NewPlayer. If not, see . 19 | */ 20 | 21 | package net.newpipe.newplayer.testapp 22 | 23 | import android.app.Application 24 | import android.util.Log 25 | import androidx.core.graphics.drawable.IconCompat 26 | import dagger.Module 27 | import dagger.Provides 28 | import dagger.hilt.InstallIn 29 | import dagger.hilt.components.SingletonComponent 30 | import kotlinx.coroutines.launch 31 | import net.newpipe.newplayer.NewPlayer 32 | import net.newpipe.newplayer.NewPlayerImpl 33 | import net.newpipe.newplayer.repository.CachingRepository 34 | import net.newpipe.newplayer.repository.PrefetchingRepository 35 | import javax.inject.Singleton 36 | 37 | 38 | @Module 39 | @InstallIn(SingletonComponent::class) 40 | object NewPlayerComponent { 41 | @Provides 42 | @Singleton 43 | fun provideNewPlayer(app: Application): NewPlayer { 44 | val player = NewPlayerImpl( 45 | app = app, 46 | repository = PrefetchingRepository(CachingRepository(TestMediaRepository(app))), 47 | notificationIcon = IconCompat.createWithResource(app, R.drawable.tinny_cools), 48 | playerActivityClass = MainActivity::class.java, 49 | rescueStreamFault = ::streamErrorHandler 50 | ) 51 | if (app is NewPlayerApp) { 52 | app.appScope.launch { 53 | while (true) { 54 | player.errorFlow.collect { e -> 55 | Log.e("NewPlayerException", e.stackTraceToString()) 56 | } 57 | } 58 | } 59 | } 60 | return player 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test-app/src/main/java/net/newpipe/newplayer/testapp/streamErrorHandler.kt: -------------------------------------------------------------------------------- 1 | package net.newpipe.newplayer.testapp 2 | 3 | import androidx.media3.common.MediaItem 4 | import net.newpipe.newplayer.repository.MediaRepository 5 | import net.newpipe.newplayer.logic.NoResponse 6 | import net.newpipe.newplayer.data.SingleSelection 7 | import net.newpipe.newplayer.logic.StreamExceptionResponse 8 | import net.newpipe.newplayer.logic.ReplaceStreamSelectionResponse 9 | import java.lang.Exception 10 | 11 | suspend fun streamErrorHandler( 12 | item: String?, 13 | mediaItem: MediaItem?, 14 | exception: Exception, 15 | repository: MediaRepository 16 | ): StreamExceptionResponse { 17 | return if (item == "faulty") { 18 | ReplaceStreamSelectionResponse(SingleSelection(repository.getStreams("6502")[0])) 19 | } else { 20 | NoResponse() 21 | } 22 | } -------------------------------------------------------------------------------- /test-app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /test-app/src/main/res/drawable/headphones.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /test-app/src/main/res/drawable/pip.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /test-app/src/main/res/drawable/tinny_cools.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /test-app/src/main/res/layout-land/activity_main.xml: -------------------------------------------------------------------------------- 1 | 20 | 21 | 28 | 29 | 40 | 41 | 51 | 52 | 61 | 62 | 63 | 64 | 65 | 75 | 76 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /test-app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 20 | 27 | 28 | 39 | 40 | 50 | 51 | 60 | 61 | 62 | 63 | 64 | 73 | 74 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /test-app/src/main/res/layout/buttons.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 13 | 14 | 20 | 21 | 27 | 28 | 29 | 30 |