├── .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 |
101 |
--------------------------------------------------------------------------------
/misc/tiny_icon_old.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
24 |
25 |
65 |
--------------------------------------------------------------------------------
/misc/tiny_placeholder.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
36 |
37 |
43 |
44 |
50 |
51 |
57 |
58 |
65 |
66 |
73 |
74 |
81 |
82 |
89 |
90 |
97 |
98 |
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TeamNewPipe/NewPlayer/2bb7e323446a38486bc6c1151e84bba83422bd63/test-app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TeamNewPipe/NewPlayer/2bb7e323446a38486bc6c1151e84bba83422bd63/test-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TeamNewPipe/NewPlayer/2bb7e323446a38486bc6c1151e84bba83422bd63/test-app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TeamNewPipe/NewPlayer/2bb7e323446a38486bc6c1151e84bba83422bd63/test-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TeamNewPipe/NewPlayer/2bb7e323446a38486bc6c1151e84bba83422bd63/test-app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TeamNewPipe/NewPlayer/2bb7e323446a38486bc6c1151e84bba83422bd63/test-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TeamNewPipe/NewPlayer/2bb7e323446a38486bc6c1151e84bba83422bd63/test-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TeamNewPipe/NewPlayer/2bb7e323446a38486bc6c1151e84bba83422bd63/test-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TeamNewPipe/NewPlayer/2bb7e323446a38486bc6c1151e84bba83422bd63/test-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TeamNewPipe/NewPlayer/2bb7e323446a38486bc6c1151e84bba83422bd63/test-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/test-app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
27 |
--------------------------------------------------------------------------------
/test-app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
22 |
23 |
24 |
25 | #FF000000
26 | #FFFFFFFF
27 |
--------------------------------------------------------------------------------
/test-app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/test-app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
30 |
33 |
--------------------------------------------------------------------------------
/test-app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
29 |
30 |
34 |
--------------------------------------------------------------------------------
/test-app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
27 |
28 |
29 |
33 |
34 |
40 |
--------------------------------------------------------------------------------