Repository: novoda/no-player Branch: master Commit: c2e0dab24229 Files: 266 Total size: 738.5 KB Directory structure: gitextract_28nzi1vk/ ├── .github/ │ ├── contributing.md │ ├── issue_template.md │ ├── pull_request_template.md │ └── workflows/ │ └── pull-request-builder.yml ├── .gitignore ├── .idea/ │ ├── codeStyleSettings.xml │ ├── compiler.xml │ ├── encodings.xml │ └── vcs.xml ├── LICENSE ├── NOTICE ├── README.md ├── build.gradle ├── core/ │ ├── build.gradle │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── novoda/ │ │ │ └── noplayer/ │ │ │ ├── AndroidMediaPlayerCapabilities.java │ │ │ ├── AspectRatioChangeCalculator.java │ │ │ ├── ContentType.java │ │ │ ├── DetailErrorType.java │ │ │ ├── ExoPlayerCapabilities.java │ │ │ ├── Listeners.java │ │ │ ├── NoPlayer.java │ │ │ ├── NoPlayerCreator.java │ │ │ ├── NoPlayerError.java │ │ │ ├── NoPlayerView.java │ │ │ ├── Options.java │ │ │ ├── OptionsBuilder.java │ │ │ ├── PlayerBuilder.java │ │ │ ├── PlayerCapabilities.java │ │ │ ├── PlayerErrorType.java │ │ │ ├── PlayerInformation.java │ │ │ ├── PlayerState.java │ │ │ ├── PlayerSurfaceHolder.java │ │ │ ├── PlayerType.java │ │ │ ├── PlayerView.java │ │ │ ├── PlayerViewSurfaceHolder.java │ │ │ ├── SubtitlePainter.java │ │ │ ├── SubtitleView.java │ │ │ ├── SurfaceRequester.java │ │ │ ├── UnableToCreatePlayerException.java │ │ │ ├── drm/ │ │ │ │ ├── DownloadedModularDrm.java │ │ │ │ ├── DrmHandler.java │ │ │ │ ├── DrmType.java │ │ │ │ ├── ModularDrmKeyRequest.java │ │ │ │ ├── ModularDrmProvisionRequest.java │ │ │ │ └── StreamingModularDrm.java │ │ │ ├── external/ │ │ │ │ └── exoplayer/ │ │ │ │ ├── text/ │ │ │ │ │ └── webvtt/ │ │ │ │ │ ├── CssParser.java │ │ │ │ │ ├── WebvttCueParser.java │ │ │ │ │ ├── WebvttDecoder.java │ │ │ │ │ └── WebvttSubtitle.java │ │ │ │ └── util/ │ │ │ │ └── ColorParser.java │ │ │ ├── internal/ │ │ │ │ ├── Clock.java │ │ │ │ ├── Heart.java │ │ │ │ ├── SystemClock.java │ │ │ │ ├── drm/ │ │ │ │ │ └── provision/ │ │ │ │ │ ├── HttpPostingProvisionExecutor.java │ │ │ │ │ ├── HttpUrlConnectionPoster.java │ │ │ │ │ ├── ProvisionExecutor.java │ │ │ │ │ ├── ProvisionExecutorCreator.java │ │ │ │ │ ├── ProvisioningCapabilities.java │ │ │ │ │ └── UnableToProvisionException.java │ │ │ │ ├── exoplayer/ │ │ │ │ │ ├── BandwidthMeterCreator.java │ │ │ │ │ ├── CompositeTrackSelector.java │ │ │ │ │ ├── CompositeTrackSelectorCreator.java │ │ │ │ │ ├── ExoPlayerCreator.java │ │ │ │ │ ├── ExoPlayerCueMapper.java │ │ │ │ │ ├── ExoPlayerFacade.java │ │ │ │ │ ├── ExoPlayerInformation.java │ │ │ │ │ ├── ExoPlayerTwoImpl.java │ │ │ │ │ ├── NoPlayerExoPlayerCreator.java │ │ │ │ │ ├── RendererTypeRequester.java │ │ │ │ │ ├── RendererTypeRequesterCreator.java │ │ │ │ │ ├── SecurityDowngradingCodecSelector.java │ │ │ │ │ ├── SimpleRenderersFactory.java │ │ │ │ │ ├── TextRendererOutput.java │ │ │ │ │ ├── drm/ │ │ │ │ │ │ ├── DownloadDrmSessionCreator.java │ │ │ │ │ │ ├── DrmSessionCreator.java │ │ │ │ │ │ ├── DrmSessionCreatorException.java │ │ │ │ │ │ ├── DrmSessionCreatorFactory.java │ │ │ │ │ │ ├── FrameworkDrmSession.java │ │ │ │ │ │ ├── FrameworkMediaDrmCreator.java │ │ │ │ │ │ ├── InvalidDrmSession.java │ │ │ │ │ │ ├── LocalDrmSession.java │ │ │ │ │ │ ├── LocalDrmSessionManager.java │ │ │ │ │ │ ├── NoDrmSessionCreator.java │ │ │ │ │ │ ├── ProvisioningModularDrmCallback.java │ │ │ │ │ │ ├── SessionId.java │ │ │ │ │ │ └── StreamingDrmSessionCreator.java │ │ │ │ │ ├── error/ │ │ │ │ │ │ ├── ErrorFormatter.java │ │ │ │ │ │ ├── ExoPlayerErrorMapper.java │ │ │ │ │ │ ├── RendererErrorMapper.java │ │ │ │ │ │ ├── SourceErrorMapper.java │ │ │ │ │ │ └── UnexpectedErrorMapper.java │ │ │ │ │ ├── forwarder/ │ │ │ │ │ │ ├── AnalyticsListenerForwarder.java │ │ │ │ │ │ ├── BitrateForwarder.java │ │ │ │ │ │ ├── BufferStateForwarder.java │ │ │ │ │ │ ├── DrmSessionInfoForwarder.java │ │ │ │ │ │ ├── EventInfoForwarder.java │ │ │ │ │ │ ├── EventListener.java │ │ │ │ │ │ ├── ExoPlayerDrmSessionEventListener.java │ │ │ │ │ │ ├── ExoPlayerForwarder.java │ │ │ │ │ │ ├── ExoPlayerVideoListener.java │ │ │ │ │ │ ├── ForwarderInformation.java │ │ │ │ │ │ ├── MediaSourceEventForwarder.java │ │ │ │ │ │ ├── NoPlayerAnalyticsListener.java │ │ │ │ │ │ ├── NoPlayerMediaSourceEventListener.java │ │ │ │ │ │ ├── OnCompletionForwarder.java │ │ │ │ │ │ ├── OnCompletionStateChangedForwarder.java │ │ │ │ │ │ ├── OnPrepareForwarder.java │ │ │ │ │ │ ├── PlayerOnErrorForwarder.java │ │ │ │ │ │ └── VideoSizeChangedForwarder.java │ │ │ │ │ └── mediasource/ │ │ │ │ │ ├── AudioTrackType.java │ │ │ │ │ ├── ExoPlayerAudioTrackSelector.java │ │ │ │ │ ├── ExoPlayerMappedTrackInfo.java │ │ │ │ │ ├── ExoPlayerSubtitleTrackSelector.java │ │ │ │ │ ├── ExoPlayerTrackSelector.java │ │ │ │ │ ├── ExoPlayerVideoTrackSelector.java │ │ │ │ │ ├── MediaSourceFactory.java │ │ │ │ │ ├── RendererTrackIndexExtractor.java │ │ │ │ │ └── TrackType.java │ │ │ │ ├── listeners/ │ │ │ │ │ ├── BitrateChangedListeners.java │ │ │ │ │ ├── BufferStateListeners.java │ │ │ │ │ ├── CompletionListeners.java │ │ │ │ │ ├── DroppedFramesListeners.java │ │ │ │ │ ├── ErrorListeners.java │ │ │ │ │ ├── HeartbeatCallbacks.java │ │ │ │ │ ├── InfoListeners.java │ │ │ │ │ ├── PlayerListenersHolder.java │ │ │ │ │ ├── PreparedListeners.java │ │ │ │ │ ├── StateChangedListeners.java │ │ │ │ │ └── VideoSizeChangedListeners.java │ │ │ │ ├── mediaplayer/ │ │ │ │ │ ├── AndroidMediaPlayerAudioTrackSelector.java │ │ │ │ │ ├── AndroidMediaPlayerFacade.java │ │ │ │ │ ├── AndroidMediaPlayerImpl.java │ │ │ │ │ ├── AndroidMediaPlayerType.java │ │ │ │ │ ├── BuggyVideoDriverPreventer.java │ │ │ │ │ ├── CheckBufferHeartbeatCallback.java │ │ │ │ │ ├── DelayedActionExecutor.java │ │ │ │ │ ├── ErrorFactory.java │ │ │ │ │ ├── ErrorFormatter.java │ │ │ │ │ ├── MediaPlayerCreator.java │ │ │ │ │ ├── MediaPlayerInformation.java │ │ │ │ │ ├── MediaPlayerTypeReader.java │ │ │ │ │ ├── NoPlayerMediaPlayerCreator.java │ │ │ │ │ ├── NoPlayerTrackInfo.java │ │ │ │ │ ├── NoPlayerTrackInfos.java │ │ │ │ │ ├── OnPotentialBuggyDriverLayoutListener.java │ │ │ │ │ ├── PlaybackStateChecker.java │ │ │ │ │ ├── SystemProperties.java │ │ │ │ │ ├── TrackInfosFactory.java │ │ │ │ │ └── forwarder/ │ │ │ │ │ ├── BufferHeartbeatListener.java │ │ │ │ │ ├── BufferInfoForwarder.java │ │ │ │ │ ├── BufferOnPreparedListener.java │ │ │ │ │ ├── CompletionForwarder.java │ │ │ │ │ ├── CompletionInfoForwarder.java │ │ │ │ │ ├── CompletionStateChangedForwarder.java │ │ │ │ │ ├── ErrorForwarder.java │ │ │ │ │ ├── ErrorInfoForwarder.java │ │ │ │ │ ├── HeartBeatListener.java │ │ │ │ │ ├── MediaPlayerCompletionListener.java │ │ │ │ │ ├── MediaPlayerErrorListener.java │ │ │ │ │ ├── MediaPlayerForwarder.java │ │ │ │ │ ├── MediaPlayerPreparedListener.java │ │ │ │ │ ├── OnPreparedForwarder.java │ │ │ │ │ ├── OnPreparedInfoForwarder.java │ │ │ │ │ ├── VideoSizeChangedForwarder.java │ │ │ │ │ ├── VideoSizeChangedInfoForwarder.java │ │ │ │ │ └── VideoSizeChangedListener.java │ │ │ │ └── utils/ │ │ │ │ ├── AndroidDeviceVersion.java │ │ │ │ ├── NoPlayerLog.java │ │ │ │ └── Optional.java │ │ │ ├── model/ │ │ │ │ ├── AudioTracks.java │ │ │ │ ├── Bitrate.java │ │ │ │ ├── Either.java │ │ │ │ ├── KeySetId.java │ │ │ │ ├── LoadTimeout.java │ │ │ │ ├── NoPlayerCue.java │ │ │ │ ├── PlayerAudioTrack.java │ │ │ │ ├── PlayerSubtitleTrack.java │ │ │ │ ├── PlayerVideoTrack.java │ │ │ │ ├── TextCues.java │ │ │ │ └── Timeout.java │ │ │ └── text/ │ │ │ └── NoPlayerSubtitleDecoderFactory.java │ │ └── res/ │ │ └── layout/ │ │ └── noplayer_view.xml │ └── test/ │ ├── java/ │ │ ├── com/ │ │ │ ├── google/ │ │ │ │ └── android/ │ │ │ │ └── exoplayer2/ │ │ │ │ ├── ExoPlaybackExceptionFactory.java │ │ │ │ └── drm/ │ │ │ │ └── FrameworkMediaCryptoFixture.java │ │ │ └── novoda/ │ │ │ └── noplayer/ │ │ │ ├── LoadTimeoutTest.java │ │ │ ├── NoPlayerCreatorTest.java │ │ │ ├── PlayerSurfaceHolderTest.java │ │ │ ├── PlayerTypeTest.java │ │ │ ├── internal/ │ │ │ │ ├── HeartTest.java │ │ │ │ ├── drm/ │ │ │ │ │ └── provision/ │ │ │ │ │ ├── HttpPostingProvisionExecutorTest.java │ │ │ │ │ └── ProvisioningCapabilitiesFixtures.java │ │ │ │ ├── exoplayer/ │ │ │ │ │ ├── ExoPlayerFacadeTest.java │ │ │ │ │ ├── ExoPlayerInformationTest.java │ │ │ │ │ ├── ExoPlayerTwoImplTest.java │ │ │ │ │ ├── NoPlayerExoPlayerCreatorTest.java │ │ │ │ │ ├── PlayerSubtitleTrackFixture.java │ │ │ │ │ ├── SecurityDowngradingCodecSelectorTest.java │ │ │ │ │ ├── drm/ │ │ │ │ │ │ ├── DrmSessionCreatorFactoryTest.java │ │ │ │ │ │ └── LocalDrmSessionManagerTest.java │ │ │ │ │ ├── error/ │ │ │ │ │ │ └── ErrorFormatterTest.java │ │ │ │ │ ├── forwarder/ │ │ │ │ │ │ └── ExoPlayerErrorMapperTest.java │ │ │ │ │ └── mediasource/ │ │ │ │ │ ├── AudioFormatFixture.java │ │ │ │ │ ├── AudioTrackTypeTest.java │ │ │ │ │ ├── ExoPlayerAudioTrackSelectorTest.java │ │ │ │ │ ├── ExoPlayerVideoTrackSelectorTest.java │ │ │ │ │ ├── RendererTrackIndexExtractorTest.java │ │ │ │ │ └── VideoFormatFixture.java │ │ │ │ ├── listeners/ │ │ │ │ │ ├── BufferStateListenersTest.java │ │ │ │ │ ├── CompletionListenersTest.java │ │ │ │ │ └── StateChangedListenersTest.java │ │ │ │ └── mediaplayer/ │ │ │ │ ├── AndroidMediaPlayerAudioTrackSelectorTest.java │ │ │ │ ├── AndroidMediaPlayerFacadeTest.java │ │ │ │ ├── AndroidMediaPlayerImplTest.java │ │ │ │ ├── BuggyVideoDriverPreventerTest.java │ │ │ │ ├── DelayedActionExecutorTest.java │ │ │ │ ├── ErrorFactoryTest.java │ │ │ │ ├── ErrorFormatterTest.java │ │ │ │ ├── LoadTimeoutTest.java │ │ │ │ ├── MediaPlayerInformationTest.java │ │ │ │ ├── NoPlayerMediaPlayerCreatorTest.java │ │ │ │ ├── OnPotentialBuggyDriverLayoutListenerTest.java │ │ │ │ ├── PlaybackStateCheckerTest.java │ │ │ │ └── PlayerCheckerTest.java │ │ │ └── model/ │ │ │ ├── AudioTracksTest.java │ │ │ ├── EitherTest.java │ │ │ ├── PlayerAudioTrackFixture.java │ │ │ └── PlayerVideoTrackFixture.java │ │ └── utils/ │ │ └── ExceptionMatcher.java │ └── resources/ │ └── mockito-extensions/ │ └── org.mockito.plugins.MockMaker ├── demo/ │ ├── build.gradle │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── novoda/ │ │ │ └── demo/ │ │ │ ├── AndroidControllerView.java │ │ │ ├── ControllerView.java │ │ │ ├── DataPostingModularDrm.java │ │ │ ├── DemoPresenter.java │ │ │ ├── DialogCreator.java │ │ │ ├── HttpClient.java │ │ │ ├── MainActivity.java │ │ │ ├── ProgressCalculator.java │ │ │ └── TimeFormatter.java │ │ └── res/ │ │ ├── drawable/ │ │ │ ├── progress.xml │ │ │ └── thumb.xml │ │ ├── layout/ │ │ │ ├── activity_main.xml │ │ │ ├── list_item.xml │ │ │ └── merge_player_controls.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ └── values/ │ │ ├── colors.xml │ │ ├── controls_styles.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test/ │ └── java/ │ └── com/ │ └── novoda/ │ └── demo/ │ └── TimeFormatterTest.java ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── team-props/ ├── static-analysis/ │ ├── checkstyle-modules.xml │ ├── checkstyle-suppressions.xml │ ├── findbugs-excludes.xml │ ├── lint-config.xml │ └── pmd-rules.xml └── static-analysis.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/contributing.md ================================================ # Contributing to Novoda's Open Source projects We encourage everyone inside and outside Novoda to contribute to the projects using Github's pull requests. ## Issuing a pull request Github makes it really easy to create a pull request (PR) against a repo. Just fork it, implement your changes and create a pull request back to the original repo. The PR should follow this format: * The title of the PR should be a short sentence explaining the fix * The PR description must contain at least two sections: 1. **The problem**: Explain what's the bug you're trying to solve or the missing feature you're trying to add with this PR. 2. **The solution**: Explain the fix or feature you've implemented in the PR. If this PR caused any UI change, then you should include screenshots or gifs showing how it looked before and after the change. See https://guides.github.com/features/mastering-markdown/ to create great looking markdown tables for showing your UI changes. 3. Feel free to include funny memes or gifs ;) ## Writing tests When you issue a PR, please take some time to consider writing tests for the issue. For example if you're solving a bug, you could write the test that reproduces the bug first, then fix the issue. This makes sure the bug doesn't come back later. Also make sure the project builds and all the tests pass before creating the PR. A non tested PR will not be merged back. ## Code formatting We use a pretty standard Java code formatting: * Use spaces instead of tabs. * Indenting as explained here: http://en.wikipedia.org/wiki/Indent_style#Variant:_1TBS * Use curly braces for everything, even for one line `if`, `for`, etc. statements. * One line of white spaces between methods. * One space before parenthesis, curly braces, equals, etc. Such as: ```java if (value == 2) { value = 3; } ``` To make it easier we've made public the IDE settings we use internally so that you can import them if you want: https://github.com/novoda/novoda/tree/master/ide-settings ================================================ FILE: .github/issue_template.md ================================================ #### Problem _Explain the problem that requires addressing_ #### Potential Solution _Explain high level any potential solutions to the problem as you see it_ #### Impact _Explain high level what is the impact of fixing (or not fixing) this issue: - Are there any other components affected? - Any future feature can benefit from this? - Is it slowing down something? ================================================ FILE: .github/pull_request_template.md ================================================ ## Problem _Explain what is requested to do, or the problem you had to fix_ ## Solution _Explain high level how have you implemented the task, add any quirks or interesting information_ ### Test(s) added _Explain what you did test and what you didn't, and why_ ### Screenshots | Before | After | | ------ | ----- | | gif/png _before_ | gif/png _after_ | ### Paired with _Specify @github-handle @or-handles, or write "Nobody" if you did not pair_ ================================================ FILE: .github/workflows/pull-request-builder.yml ================================================ name: Pull Request Builder on: pull_request: env: GRADLE_USER_HOME: .gradle jobs: build: runs-on: ubuntu-latest steps: - name: Checkout source-code uses: actions/checkout@v1 with: fetch-depth: 1 - name: Set up JDK 1.8 uses: actions/setup-java@v1 with: java-version: 1.8 - name: Synchronise cache 1/2 uses: actions/cache@v1 with: path: .gradle/wrapper key: gradle-cache-wrapper-${{ hashFiles('core/build.gradle') }} - name: Synchronise cache 2/2 uses: actions/cache@v1 with: path: .gradle/caches key: gradle-cache-caches-${{ hashFiles('core/build.gradle') }} - name: Build run: ./gradlew --no-daemon evaluateViolations lint test - name: Gather results if: success() || failure() run: | mkdir -p artifacts/core | mkdir -p artifacts/demo | cp -r core/build/reports/* artifacts/core | cp -r demo/build/reports/* artifacts/demo - name: Upload results uses: actions/upload-artifact@v1.0.0 if: success() || failure() with: name: results path: artifacts ================================================ FILE: .gitignore ================================================ # Built application files *.apk *.ap_ # Files for the Dalvik VM *.dex # Java class files *.class # Generated files bin/ gen/ # Gradle files .gradle/ build/ # Local configuration file (sdk path, etc) local.properties # Proguard folder generated by Eclipse proguard/ # Log Files *.log # IDEA/Android Studio ignores *.iml *.ipr *.iws **/.idea/ # IDEA/Android Studio Ignore exceptions !/.idea/copyright/ !/.idea/fileTemplates/ !/.idea/inspectionProfiles/ !/.idea/scopes/ !/.idea/codeStyleSettings.xml !/.idea/compiler.xml !/.idea/encodings.xml !/.idea/vcs.xml # OSX *.DS_Store # Heap dump captures captures/ ================================================ FILE: .idea/codeStyleSettings.xml ================================================ ================================================ FILE: .idea/compiler.xml ================================================ ================================================ FILE: .idea/encodings.xml ================================================ ================================================ FILE: .idea/vcs.xml ================================================ ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: NOTICE ================================================ Copyright 2017 Novoda Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # This project is no longer under maintenance - 13/7/2020 Upgrades to latests version of `ExoPlayer` requires a significative amount of changes. The project is no longer maintained from our end. ----- ![noplayer](art/noplayer-header.png) [![CI status](https://github.com/novoda/no-player/workflows/Production%20Builder/badge.svg)](https://github.com/novoda/no-player/actions?query=workflow%3A%22Production+Builder%22) [![Download from Bintray](https://api.bintray.com/packages/novoda/maven/no-player/images/download.svg)](https://bintray.com/novoda/maven/no-player/_latestVersion) ![Tests](https://img.shields.io/jenkins/t/https/ci.novoda.com/view/Open%20source/job/no-player.svg) ![Coverage](https://img.shields.io/jenkins/j/https/ci.novoda.com/view/Open%20source/job/no-player.svg) [![Apache 2.0 Licence](https://img.shields.io/github/license/novoda/no-player.svg)](https://github.com/novoda/no-player/blob/master/LICENSE) A simplified Android `Player` wrapper for [MediaPlayer](https://developer.android.com/reference/android/media/MediaPlayer.html) and [ExoPlayer](https://google.github.io/ExoPlayer/). ## Description Some of the benefits are: - Unified playback interface and event listeners for ExoPlayer and MediaPlayer - `MediaPlayer` buffering - `ExoPlayer` local, streaming and provisioning WideVine Modular DRM - Maintains video Aspect Ratio by default - Player selection based on `ContentType` and DRM Experimental Features, use with caution: - Support for TextureView ## Adding to your project To start using this library, add these lines to the `build.gradle` of your project: ```groovy repositories { jcenter() } dependencies { implementation 'com.novoda:no-player:' } ``` From no-player 4.5.0 this is also needed in the android section of your `build.gradle` ```groovy compileOptions { targetCompatibility JavaVersion.VERSION_1_8 } ``` ### Simple usage 1. Create a `Player`: ```java Player player = new PlayerBuilder() .withPriority(PlayerType.EXO_PLAYER) .withWidevineModularStreamingDrm(drmHandler) .build(this); ``` 2. Create the `PlayerView`: ```xml ``` 3. Attach to a `PlayerView`: ```java PlayerView playerView = findViewById(R.id.player_view); player.attach(playerView); ``` 4. Play some content: ```java player.getListeners().addPreparedListener(playerState -> player.play()); Uri uri = Uri.parse(mpdUrl); player.loadVideo(uri, ContentType.DASH); ``` ## Snapshots [![CI status](https://github.com/novoda/no-player/workflows/Snapshot%20Builder/badge.svg)](https://github.com/novoda/no-player/actions?query=workflow%3A%22Snapshot+Builder%22) [![Download from Bintray](https://api.bintray.com/packages/novoda-oss/snapshots/no-player/images/download.svg)](https://bintray.com/novoda-oss/snapshots/no-player/_latestVersion) Snapshot builds from [`develop`](https://github.com/novoda/no-player/compare/master...develop) are automatically deployed to a [repository](https://bintray.com/novoda-oss/snapshots/no-player/_latestVersion) that is not synced with JCenter. To consume a snapshot build add an additional maven repo as follows: ``` repositories { maven { url 'https://dl.bintray.com/novoda-oss/snapshots/' } } ``` You can find the latest snapshot version following this [link](https://bintray.com/novoda-oss/snapshots/no-player/_latestVersion). ## Contributing We always welcome people to contribute new features or bug fixes, [here is how](https://github.com/novoda/novoda/blob/master/CONTRIBUTING.md). If you have a problem, check the [Issues Page](https://github.com/novoda/no-player/issues) first to see if we are already working on it. Looking for community help? Browse the already asked [Stack Overflow Questions](http://stackoverflow.com/questions/tagged/support-no-player) or use the tag `support-no-player` when posting a new question. ================================================ FILE: build.gradle ================================================ allprojects { version = '4.5.4' } def teamPropsFile(propsFile) { def teamPropsDir = rootProject.file('team-props') return new File(teamPropsDir, propsFile) } buildscript { repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.3.1' classpath 'com.novoda:bintray-release:0.9' classpath 'com.novoda:gradle-static-analysis-plugin:0.8' classpath 'com.novoda:gradle-build-properties-plugin:0.4.1' classpath "com.github.ben-manes:gradle-versions-plugin:0.20.0" } } subprojects { repositories { google() jcenter() } apply from: teamPropsFile('static-analysis.gradle') } allprojects { apply plugin: 'com.github.ben-manes.versions' dependencyUpdates.resolutionStrategy { componentSelection { rules -> rules.all { ComponentSelection selection -> boolean rejected = ['alpha', 'beta', 'rc', 'cr', 'm'].any { qualifier -> selection.candidate.version ==~ /(?i).*[.-]${qualifier}[.\d-]*/ } if (rejected) { selection.reject('Release candidate') } } } } } ================================================ FILE: core/build.gradle ================================================ apply plugin: 'com.android.library' apply plugin: 'bintray-release' apply plugin: 'jacoco' apply plugin: 'com.novoda.build-properties' buildProperties { cli { using(project) } bintray { def bintrayCredentials = { boolean isDryRun = cli['dryRun'].or(true).boolean return isDryRun ? ['bintrayRepo': 'n/a', 'bintrayUser': 'n/a', 'bintrayKey': 'n/a'] : new File("${System.getenv('BINTRAY_PROPERTIES')}") } using(bintrayCredentials()).or(cli) description = '''This should contain the following properties: | - bintrayRepo: name of the repo of the organisation to deploy the artifacts to | - bintrayUser: name of the account used to deploy the artifacts | - bintrayKey: API key of the account used to deploy the artifacts '''.stripMargin() } publish { def generateVersion = { boolean isSnapshot = cli['bintraySnapshot'].or(false).boolean if (isSnapshot) { return "SNAPSHOT-${System.getenv('BUILD_NUMBER') ?: 'LOCAL'}"; } boolean isExperimental = cli['bintrayExperimental'].or(false).boolean if (isExperimental) { return "EXPERIMENTAL-${System.getenv('BUILD_NUMBER') ?: 'LOCAL'}"; } return version } using(['version': "${generateVersion()}"]) .or(buildProperties.bintray) } } android { compileSdkVersion 28 buildToolsVersion '28.0.3' defaultConfig { minSdkVersion 16 testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner' } lintOptions { lintConfig teamPropsFile('static-analysis/lint-config.xml') abortOnError true warningsAsErrors true } compileOptions { targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation 'com.google.android.exoplayer:exoplayer:2.9.6' implementation 'com.android.support:support-annotations:28.0.0' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.27.0' testImplementation 'org.easytesting:fest-assert-core:2.0M10' } publish { userOrg = 'novoda' repoName = buildProperties.publish['bintrayRepo'].string groupId = 'com.novoda' artifactId = 'no-player' version = buildProperties.publish['version'].string bintrayUser = buildProperties.publish['bintrayUser'].string bintrayKey = buildProperties.publish['bintrayKey'].string publishVersion = version uploadName = 'no-player' desc = 'player to wrap players' website = 'https://github.com/novoda/no-player' } ================================================ FILE: core/src/main/AndroidManifest.xml ================================================ ================================================ FILE: core/src/main/java/com/novoda/noplayer/AndroidMediaPlayerCapabilities.java ================================================ package com.novoda.noplayer; import com.novoda.noplayer.drm.DrmType; import java.util.Arrays; import java.util.List; class AndroidMediaPlayerCapabilities implements PlayerCapabilities { private static final List SUPPORTED_DRM_TYPES = Arrays.asList(DrmType.NONE, DrmType.WIDEVINE_CLASSIC); @Override public boolean supports(DrmType drmType) { return SUPPORTED_DRM_TYPES.contains(drmType); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/AspectRatioChangeCalculator.java ================================================ package com.novoda.noplayer; class AspectRatioChangeCalculator { private final Listener listener; AspectRatioChangeCalculator(Listener listener) { this.listener = listener; } void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio) { float aspectRatio = determineAspectRatio(width, height, pixelWidthHeightRatio); listener.onNewAspectRatio(aspectRatio); } private float determineAspectRatio(int videoWidth, int videoHeight, float pixelWidthHeightRatio) { if (videoHeight == 0) { return 1; } return (videoWidth * pixelWidthHeightRatio) / videoHeight; } interface Listener { void onNewAspectRatio(float aspectRatio); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/ContentType.java ================================================ package com.novoda.noplayer; public enum ContentType { H264, DASH, HLS } ================================================ FILE: core/src/main/java/com/novoda/noplayer/DetailErrorType.java ================================================ package com.novoda.noplayer; /** * Assume all errors are thrown by Exo Player, MEDIA_PLAYER prefix will * indicate that the error is thrown by Media Player. */ public enum DetailErrorType { // SOURCE, LIVE_STALE_MANIFEST_AND_NEW_MANIFEST_COULD_NOT_LOAD_ERROR, PARSING_MEDIA_DATA_OR_METADATA_ERROR, AD_LOAD_ERROR_THEN_WILL_SKIP, AD_GROUP_LOAD_ERROR_THEN_WILL_SKIP, ALL_ADS_LOAD_ERROR_THEN_WILL_SKIP, ADS_LOAD_UNEXPECTED_ERROR_THEN_WILL_SKIP, CLIPPING_MEDIA_SOURCE_CANNOT_CLIP_WRAPPED_SOURCE_INVALID_PERIOD_COUNT, CLIPPING_MEDIA_SOURCE_CANNOT_CLIP_WRAPPED_SOURCE_NOT_SEEKABLE_TO_START, CLIPPING_MEDIA_SOURCE_CANNOT_CLIP_WRAPPED_SOURCE_START_EXCEEDS_END, CLIPPING_MEDIA_SOURCE_CANNOT_CLIP_WRAPPED_SOURCE_UNKNOWN_ERROR, DATA_POSITION_OUT_OF_RANGE_ERROR, SAMPLE_QUEUE_MAPPING_ERROR, READING_LOCAL_FILE_ERROR, UNEXPECTED_LOADING_ERROR, DOWNLOAD_ERROR, MERGING_MEDIA_SOURCE_CANNOT_MERGE_ITS_SOURCES, TASK_CANNOT_PROCEED_PRIORITY_TOO_LOW, CACHE_WRITING_DATA_ERROR, READ_LOCAL_ASSET_ERROR, MEDIA_PLAYER_IO, MEDIA_PLAYER_MALFORMED, MEDIA_PLAYER_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK, MEDIA_PLAYER_INFO_NOT_SEEKABLE, MEDIA_PLAYER_SUBTITLE_TIMED_OUT, MEDIA_PLAYER_UNSUPPORTED_SUBTITLE, // CONNECTIVITY HTTP_CANNOT_OPEN_ERROR, HTTP_CANNOT_READ_ERROR, HTTP_CANNOT_CLOSE_ERROR, READ_CONTENT_URI_ERROR, READ_FROM_UDP_ERROR, HLS_PLAYLIST_STUCK_SERVER_SIDE_ERROR, HLS_PLAYLIST_SERVER_HAS_RESET, MEDIA_PLAYER_TIMED_OUT, // DRM UNSUPPORTED_DRM_SCHEME_ERROR, DRM_INSTANTIATION_ERROR, DRM_UNKNOWN_ERROR, CANNOT_ACQUIRE_DRM_SESSION_MISSING_SCHEME_FOR_REQUIRED_UUID_ERROR, DRM_SESSION_ERROR, DRM_KEYS_EXPIRED_ERROR, MEDIA_REQUIRES_DRM_SESSION_MANAGER_ERROR, MEDIA_PLAYER_SERVER_DIED, MEDIA_PLAYER_PREPARE_DRM_STATUS_PREPARATION_ERROR, MEDIA_PLAYER_PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR, MEDIA_PLAYER_PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR, // CONTENT_DECRYPTION FAIL_DECRYPT_DATA_DUE_NON_PLATFORM_COMPONENT_ERROR, INSUFFICIENT_OUTPUT_PROTECTION_ERROR, KEY_EXPIRED_ERROR, KEY_NOT_FOUND_WHEN_DECRYPTION_ERROR, RESOURCE_BUSY_ERROR_THEN_SHOULD_RETRY, ATTEMPTED_ON_CLOSED_SEDDION_ERROR, LICENSE_POLICY_REQUIRED_NOT_SUPPORTED_BY_DEVICE_ERROR, // RENDERER_DECODER AUDIO_SINK_CONFIGURATION_ERROR, AUDIO_SINK_INITIALISATION_ERROR, AUDIO_SINK_WRITE_ERROR, AUDIO_UNHANDLED_FORMAT_ERROR, AUDIO_DECODER_ERROR, INITIALISATION_ERROR, DECODING_SUBTITLE_ERROR, MEDIA_PLAYER_INFO_AUDIO_NOT_PLAYING, MEDIA_PLAYER_BAD_INTERLEAVING, MEDIA_PLAYER_INFO_VIDEO_NOT_PLAYING, MEDIA_PLAYER_INFO_VIDEO_TRACK_LAGGING, UNEXPECTED_CODEC_ERROR, // UNEXPECTED EGL_OPERATION_ERROR, SPURIOUS_AUDIO_TRACK_TIMESTAMP_ERROR, MULTIPLE_RENDERER_MEDIA_CLOCK_ENABLED_ERROR, UNKNOWN, MEDIA_PLAYER_UNKNOWN } ================================================ FILE: core/src/main/java/com/novoda/noplayer/ExoPlayerCapabilities.java ================================================ package com.novoda.noplayer; import com.novoda.noplayer.drm.DrmType; import java.util.Arrays; import java.util.List; class ExoPlayerCapabilities implements PlayerCapabilities { private static final List SUPPORTED_DRM_TYPES = Arrays.asList( DrmType.NONE, DrmType.WIDEVINE_MODULAR_STREAM, DrmType.WIDEVINE_MODULAR_DOWNLOAD ); @Override public boolean supports(DrmType drmType) { return SUPPORTED_DRM_TYPES.contains(drmType); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/Listeners.java ================================================ package com.novoda.noplayer; public interface Listeners { /** * Add an {@link NoPlayer.ErrorListener} to be notified of Player errors. * * @param errorListener to notify. */ void addErrorListener(NoPlayer.ErrorListener errorListener); /** * Remove a given {@link NoPlayer.ErrorListener}. * * @param errorListener to remove. */ void removeErrorListener(NoPlayer.ErrorListener errorListener); /** * Add a {@link NoPlayer.PreparedListener} to be notified when the {@link NoPlayer} * is prepared and playback can begin. * * @param preparedListener to notify. */ void addPreparedListener(NoPlayer.PreparedListener preparedListener); /** * Remove a given {@link NoPlayer.PreparedListener}. * * @param preparedListener to remove. */ void removePreparedListener(NoPlayer.PreparedListener preparedListener); /** * Add a {@link NoPlayer.BufferStateListener} to be notified of buffer state events. * * @param bufferStateListener to notify. */ void addBufferStateListener(NoPlayer.BufferStateListener bufferStateListener); /** * Remove a given {@link NoPlayer.BufferStateListener}. * * @param bufferStateListener to remove. */ void removeBufferStateListener(NoPlayer.BufferStateListener bufferStateListener); /** * Add a {@link NoPlayer.CompletionListener} to be notified when a media asset has completed playback. * * @param completionListener to notify. */ void addCompletionListener(NoPlayer.CompletionListener completionListener); /** * Remove a given {@link NoPlayer.CompletionListener}. * * @param completionListener to remove. */ void removeCompletionListener(NoPlayer.CompletionListener completionListener); /** * Add a {@link NoPlayer.StateChangedListener} to be notified of Player state changes. * e.g. Play/Pause/Stop * * @param stateChangedListener to notify. */ void addStateChangedListener(NoPlayer.StateChangedListener stateChangedListener); /** * Remove a given {@link NoPlayer.StateChangedListener}. * * @param stateChangedListener to remove. */ void removeStateChangedListener(NoPlayer.StateChangedListener stateChangedListener); /** * Add an {@link NoPlayer.InfoListener} to be notified of internal player callbacks * with additional information. * * @param infoListener to notify. */ void addInfoListener(NoPlayer.InfoListener infoListener); /** * Remove a given {@link NoPlayer.InfoListener}. * * @param infoListener to remove. */ void removeInfoListener(NoPlayer.InfoListener infoListener); /** * Add a {@link NoPlayer.BitrateChangedListener} to be notified of video and audio bitrate changes. * * @param bitrateChangedListener to notify. */ void addBitrateChangedListener(NoPlayer.BitrateChangedListener bitrateChangedListener); /** * Remove a given {@link NoPlayer.BitrateChangedListener}. * * @param bitrateChangedListener to remove. */ void removeBitrateChangedListener(NoPlayer.BitrateChangedListener bitrateChangedListener); /** * Add a {@link NoPlayer.HeartbeatCallback} to be notified on every tick of playback with a {@link NoPlayer}. * * @param heartbeatCallback to notify. */ void addHeartbeatCallback(NoPlayer.HeartbeatCallback heartbeatCallback); /** * Remove a given {@link NoPlayer.HeartbeatCallback}. * * @param heartbeatCallback to remove. */ void removeHeartbeatCallback(NoPlayer.HeartbeatCallback heartbeatCallback); /** * Add a {@link NoPlayer.VideoSizeChangedListener} to be notified whenever the video size changes. * * @param videoSizeChangedListener to notify. */ void addVideoSizeChangedListener(NoPlayer.VideoSizeChangedListener videoSizeChangedListener); /** * Remove a given {@link NoPlayer.VideoSizeChangedListener}. * * @param videoSizeChangedListener to remove. */ void removeVideoSizeChangedListener(NoPlayer.VideoSizeChangedListener videoSizeChangedListener); /** * Add a given {@link NoPlayer.DroppedVideoFramesListener} to be notified when video playback drops frames * * @param droppedVideoFramesListener to notify */ void addDroppedVideoFrames(NoPlayer.DroppedVideoFramesListener droppedVideoFramesListener); /** * Remove a given {@link NoPlayer.DroppedVideoFramesListener}. * * @param droppedVideoFramesListener to remove. */ void removeDroppedVideoFrames(NoPlayer.DroppedVideoFramesListener droppedVideoFramesListener); } ================================================ FILE: core/src/main/java/com/novoda/noplayer/NoPlayer.java ================================================ package com.novoda.noplayer; import android.net.Uri; import android.support.annotation.FloatRange; import com.novoda.noplayer.internal.utils.Optional; import com.novoda.noplayer.model.AudioTracks; import com.novoda.noplayer.model.Bitrate; import com.novoda.noplayer.model.PlayerAudioTrack; import com.novoda.noplayer.model.PlayerSubtitleTrack; import com.novoda.noplayer.model.PlayerVideoTrack; import com.novoda.noplayer.model.Timeout; import java.util.List; import java.util.Map; public interface NoPlayer extends PlayerState { /** * Retrieves a holder, which allows you to add and remove listeners on the Player. * * @return {@link Listeners} holder. */ Listeners getListeners(); /** * Plays content of a prepared Player. * * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}. * @see NoPlayer.PreparedListener */ void play() throws IllegalStateException; /** * Deprecated: This does not perform the way it was originally intended. A seek can, and most likely will, * occur after the content has already started playing. This can lead to some unexpected behaviour. * Plays content of a prepared Player at a given position. Use {@link #loadVideo(Uri, Options)} passing * a initial position to the {@link Options}. * * @param positionInMillis to start playing content from. * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}. * @see NoPlayer.PreparedListener */ @Deprecated void playAt(long positionInMillis) throws IllegalStateException; /** * Pauses content of a prepared Player. * * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}. * @see NoPlayer.PreparedListener */ void pause() throws IllegalStateException; /** * Seeks content of a prepared Player to a given position. * Will not cause content to play if not already playing. * * @param positionInMillis to seek content to. * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}. * @see NoPlayer.PreparedListener */ void seekTo(long positionInMillis) throws IllegalStateException; /** * Stops playback of content and then requires call to {@link NoPlayer#loadVideo(Uri, Options)} to continue playback. */ void stop(); /** * Stops playback of content and drops all internal resources. The instance of Player should not be * used after calling release. */ void release(); /** * Loads the video content and triggers the {@link NoPlayer.PreparedListener}. * * @param uri link to the content. * @param options to be passed to the underlying player. * @throws IllegalStateException - if called before {@link NoPlayer#attach(PlayerView)}. */ void loadVideo(Uri uri, Options options) throws IllegalStateException; /** * Loads the video content and triggers the {@link NoPlayer.PreparedListener}. * * @param uri link to the content. * @param options to be passed to the underlying player. * @param timeout amount of time to wait before triggering {@link LoadTimeoutCallback}. * @param loadTimeoutCallback callback when loading has hit the timeout. * @throws IllegalStateException - if called before {@link NoPlayer#attach(PlayerView)}. */ void loadVideoWithTimeout(Uri uri, Options options, Timeout timeout, LoadTimeoutCallback loadTimeoutCallback); /** * Supplies information about the underlying player. * * @return {@link PlayerInformation}. */ PlayerInformation getPlayerInformation(); /** * Attaches a given {@link PlayerView} to the Player. * * @param playerView for displaying video content. */ void attach(PlayerView playerView); /** * Detaches a given {@link PlayerView} from the Player. * * @param playerView for displaying video content. */ void detach(PlayerView playerView); /** * Retrieves all of the available {@link PlayerVideoTrack} of a prepared Player. * * @return a list of available {@link PlayerVideoTrack} of a prepared Player. * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}. * @see NoPlayer.PreparedListener */ List getVideoTracks() throws IllegalStateException; /** * Selects a given {@link PlayerVideoTrack}. * * @param videoTrack the video track to select. * @return whether the selection was successful. * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}. */ boolean selectVideoTrack(PlayerVideoTrack videoTrack) throws IllegalStateException; /** * Retrieves the currently playing {@link PlayerVideoTrack} of a prepared Player wrapped * as an {@link Optional} or {@link Optional#absent()} if unavailable. * * @return {@link PlayerVideoTrack}. * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}. * @see NoPlayer.PreparedListener */ Optional getSelectedVideoTrack() throws IllegalStateException; /** * Clears the {@link PlayerVideoTrack} selection made in {@link NoPlayer#selectVideoTrack(PlayerVideoTrack)}. * * @return whether the clear was successful. * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}. */ boolean clearVideoTrackSelection() throws IllegalStateException; /** * Retrieves all of the available {@link PlayerAudioTrack} of a prepared Player. * * @return {@link AudioTracks} that contains a list of available {@link PlayerAudioTrack}. * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}. * @see NoPlayer.PreparedListener */ AudioTracks getAudioTracks() throws IllegalStateException; /** * Selects a given {@link PlayerAudioTrack}. * * @param audioTrack the audio track to select. * @return whether the selection was successful. * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}. */ boolean selectAudioTrack(PlayerAudioTrack audioTrack) throws IllegalStateException; /** * Clears the {@link PlayerAudioTrack} selection made in {@link NoPlayer#selectAudioTrack(PlayerAudioTrack)}. * * @return whether the clear was successful. * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}. */ boolean clearAudioTrackSelection() throws IllegalStateException; /** * Retrieves all of the available {@link PlayerSubtitleTrack} of a prepared Player. * * @return A list of available {@link PlayerSubtitleTrack}. * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}. * @see NoPlayer.PreparedListener */ List getSubtitleTracks() throws IllegalStateException; /** * Selects and shows a given {@link PlayerSubtitleTrack} on an attached PlayerView. * * @param subtitleTrack the subtitle track to select. * @return whether the selection was successful. * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}. */ boolean showSubtitleTrack(PlayerSubtitleTrack subtitleTrack) throws IllegalStateException; /** * Clear and hide the subtitles on an attached PlayerView. * * @return whether the hide was successful. * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}. */ boolean hideSubtitleTrack() throws IllegalStateException; /** * Set the Player to repeat the content. * * @param repeating true to set repeating, false to disable. * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}. */ void setRepeating(boolean repeating) throws IllegalStateException; /** * Set the audio volume, with 0 being silence and 1 being unity gain. * * @param volume The audio volume. * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}. */ void setVolume(@FloatRange(from = 0.0f, to = 1.0f) float volume) throws IllegalStateException; /** * Return the audio volume, with 0 being silence and 1 being unity gain. * * @return audio volume. * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}. */ @FloatRange(from = 0.0f, to = 1.0f) float getVolume() throws IllegalStateException; /** * Clears the maximum video bitrate, if set. */ void clearMaxVideoBitrate(); /** * Sets a maximum video bitrate. If the content is playing, the video will switch to a different quality. * * @param maxVideoBitrate The maximum video bitrate in bit per second. */ void setMaxVideoBitrate(int maxVideoBitrate); interface PlayerError { PlayerErrorType type(); DetailErrorType detailType(); String message(); } interface ErrorListener { void onError(PlayerError error); } interface PreparedListener { void onPrepared(PlayerState playerState); } interface BufferStateListener { void onBufferStarted(); void onBufferCompleted(); } interface CompletionListener { void onCompletion(); } interface StateChangedListener { void onVideoPlaying(); void onVideoPaused(); void onVideoStopped(); } interface BitrateChangedListener { void onBitrateChanged(Bitrate audioBitrate, Bitrate videoBitrate); } interface VideoSizeChangedListener { void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio); } /** * A listener for debugging information. */ interface InfoListener { /** * All event listeners attached to implementations of {@link NoPlayer} will * forward information through this to provide debugging * information to client applications. * * @param callingMethod The method name from where this call originated. * @param callingMethodParams Parameter name and value pairs from where this call originated. * Pass only string representations not whole objects. */ void onNewInfo(String callingMethod, Map callingMethodParams); } interface LoadTimeoutCallback { LoadTimeoutCallback NULL_IMPL = new LoadTimeoutCallback() { @Override public void onLoadTimeout() { // do nothing } }; void onLoadTimeout(); } interface HeartbeatCallback { void onBeat(NoPlayer player); } interface DroppedVideoFramesListener { void onDroppedVideoFrames(int droppedFrames, long elapsedMsSinceLastDroppedFrames); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/NoPlayerCreator.java ================================================ package com.novoda.noplayer; import android.content.Context; import com.novoda.noplayer.drm.DrmHandler; import com.novoda.noplayer.drm.DrmType; import com.novoda.noplayer.internal.exoplayer.NoPlayerExoPlayerCreator; import com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreator; import com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreatorException; import com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreatorFactory; import com.novoda.noplayer.internal.mediaplayer.NoPlayerMediaPlayerCreator; import java.util.List; class NoPlayerCreator { private final Context context; private final List prioritizedPlayerTypes; private final NoPlayerExoPlayerCreator noPlayerExoPlayerCreator; private final NoPlayerMediaPlayerCreator noPlayerMediaPlayerCreator; private final DrmSessionCreatorFactory drmSessionCreatorFactory; NoPlayerCreator(Context context, List prioritizedPlayerTypes, NoPlayerExoPlayerCreator noPlayerExoPlayerCreator, NoPlayerMediaPlayerCreator noPlayerMediaPlayerCreator, DrmSessionCreatorFactory drmSessionCreatorFactory) { this.context = context; this.prioritizedPlayerTypes = prioritizedPlayerTypes; this.noPlayerExoPlayerCreator = noPlayerExoPlayerCreator; this.noPlayerMediaPlayerCreator = noPlayerMediaPlayerCreator; this.drmSessionCreatorFactory = drmSessionCreatorFactory; } NoPlayer create(DrmType drmType, DrmHandler drmHandler, boolean downgradeSecureDecoder, boolean allowCrossProtocolRedirects) { for (PlayerType player : prioritizedPlayerTypes) { if (player.supports(drmType)) { return createPlayerForType(player, drmType, drmHandler, downgradeSecureDecoder, allowCrossProtocolRedirects); } } throw UnableToCreatePlayerException.unhandledDrmType(drmType); } private NoPlayer createPlayerForType(PlayerType playerType, DrmType drmType, DrmHandler drmHandler, boolean downgradeSecureDecoder, boolean allowCrossProtocolRedirects) { switch (playerType) { case MEDIA_PLAYER: return noPlayerMediaPlayerCreator.createMediaPlayer(context); case EXO_PLAYER: try { DrmSessionCreator drmSessionCreator = drmSessionCreatorFactory.createFor(drmType, drmHandler); return noPlayerExoPlayerCreator.createExoPlayer( context, drmSessionCreator, downgradeSecureDecoder, allowCrossProtocolRedirects ); } catch (DrmSessionCreatorException exception) { throw new UnableToCreatePlayerException(exception); } default: throw UnableToCreatePlayerException.unhandledPlayerType(playerType); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/NoPlayerError.java ================================================ package com.novoda.noplayer; public class NoPlayerError implements NoPlayer.PlayerError { private final PlayerErrorType playerErrorType; private final DetailErrorType detailErrorType; private final String message; public NoPlayerError(PlayerErrorType playerErrorType, DetailErrorType detailErrorType, String message) { this.playerErrorType = playerErrorType; this.detailErrorType = detailErrorType; this.message = message; } @Override public PlayerErrorType type() { return playerErrorType; } @Override public DetailErrorType detailType() { return detailErrorType; } @Override public String message() { return message; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/NoPlayerView.java ================================================ package com.novoda.noplayer; import android.content.Context; import android.util.AttributeSet; import android.view.SurfaceView; import android.view.View; import android.widget.FrameLayout; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.novoda.noplayer.model.TextCues; public class NoPlayerView extends FrameLayout implements AspectRatioChangeCalculator.Listener, PlayerView { private final AspectRatioChangeCalculator aspectRatioChangeCalculator; private AspectRatioFrameLayout videoFrame; private SurfaceView surfaceView; private SubtitleView subtitleView; private View shutterView; private PlayerSurfaceHolder playerSurfaceHolder; public NoPlayerView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public NoPlayerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); aspectRatioChangeCalculator = new AspectRatioChangeCalculator(this); } @Override protected void onFinishInflate() { super.onFinishInflate(); View.inflate(getContext(), R.layout.noplayer_view, this); videoFrame = findViewById(R.id.video_frame); shutterView = findViewById(R.id.shutter); surfaceView = findViewById(R.id.surface_view); subtitleView = findViewById(R.id.subtitles_layout); playerSurfaceHolder = PlayerSurfaceHolder.create(surfaceView); } @Override public void onNewAspectRatio(float aspectRatio) { videoFrame.setAspectRatio(aspectRatio); } @Override public View getContainerView() { return surfaceView; } @Override public PlayerSurfaceHolder getPlayerSurfaceHolder() { return playerSurfaceHolder; } @Override public NoPlayer.VideoSizeChangedListener getVideoSizeChangedListener() { return videoSizeChangedListener; } @Override public NoPlayer.StateChangedListener getStateChangedListener() { return stateChangedListener; } @Override public void showSubtitles() { subtitleView.setVisibility(VISIBLE); } @Override public void hideSubtitles() { subtitleView.setVisibility(GONE); } @Override public void setSubtitleCue(TextCues textCues) { subtitleView.setCues(textCues); } private final NoPlayer.VideoSizeChangedListener videoSizeChangedListener = new NoPlayer.VideoSizeChangedListener() { @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { aspectRatioChangeCalculator.onVideoSizeChanged(width, height, pixelWidthHeightRatio); } }; private final NoPlayer.StateChangedListener stateChangedListener = new NoPlayer.StateChangedListener() { @Override public void onVideoPlaying() { shutterView.setVisibility(INVISIBLE); } @Override public void onVideoPaused() { // We don't care } @Override public void onVideoStopped() { shutterView.setVisibility(VISIBLE); } }; } ================================================ FILE: core/src/main/java/com/novoda/noplayer/Options.java ================================================ package com.novoda.noplayer; import com.novoda.noplayer.internal.utils.Optional; /** * Options to customise the underlying player. */ public class Options { private final ContentType contentType; private final int minDurationBeforeQualityIncreaseInMillis; private final int maxInitialBitrate; private final int maxVideoBitrate; private final Optional initialPositionInMillis; /** * Creates a {@link OptionsBuilder} from this Options. * * @return a new instance of {@link OptionsBuilder}. */ public OptionsBuilder toOptionsBuilder() { OptionsBuilder optionsBuilder = new OptionsBuilder() .withContentType(contentType) .withMinDurationBeforeQualityIncreaseInMillis(minDurationBeforeQualityIncreaseInMillis) .withMaxInitialBitrate(maxInitialBitrate) .withMaxVideoBitrate(maxVideoBitrate); if (initialPositionInMillis.isPresent()) { optionsBuilder = optionsBuilder.withInitialPositionInMillis(initialPositionInMillis.get()); } return optionsBuilder; } Options(ContentType contentType, int minDurationBeforeQualityIncreaseInMillis, int maxInitialBitrate, int maxVideoBitrate, Optional initialPositionInMillis) { this.contentType = contentType; this.minDurationBeforeQualityIncreaseInMillis = minDurationBeforeQualityIncreaseInMillis; this.maxInitialBitrate = maxInitialBitrate; this.maxVideoBitrate = maxVideoBitrate; this.initialPositionInMillis = initialPositionInMillis; } public ContentType contentType() { return contentType; } public int minDurationBeforeQualityIncreaseInMillis() { return minDurationBeforeQualityIncreaseInMillis; } public int maxInitialBitrate() { return maxInitialBitrate; } public int maxVideoBitrate() { return maxVideoBitrate; } public Optional getInitialPositionInMillis() { return initialPositionInMillis; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Options options = (Options) o; if (minDurationBeforeQualityIncreaseInMillis != options.minDurationBeforeQualityIncreaseInMillis) { return false; } if (maxInitialBitrate != options.maxInitialBitrate) { return false; } if (maxVideoBitrate != options.maxVideoBitrate) { return false; } if (contentType != options.contentType) { return false; } return initialPositionInMillis != null ? initialPositionInMillis.equals(options.initialPositionInMillis) : options.initialPositionInMillis == null; } @Override public int hashCode() { int result = contentType != null ? contentType.hashCode() : 0; result = 31 * result + minDurationBeforeQualityIncreaseInMillis; result = 31 * result + maxInitialBitrate; result = 31 * result + maxVideoBitrate; result = 31 * result + (initialPositionInMillis != null ? initialPositionInMillis.hashCode() : 0); return result; } @Override public String toString() { return "Options{" + "contentType=" + contentType + ", minDurationBeforeQualityIncreaseInMillis=" + minDurationBeforeQualityIncreaseInMillis + ", maxInitialBitrate=" + maxInitialBitrate + ", maxVideoBitrate=" + maxVideoBitrate + ", initialPositionInMillis=" + initialPositionInMillis + '}'; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/OptionsBuilder.java ================================================ package com.novoda.noplayer; import android.net.Uri; import com.novoda.noplayer.internal.utils.Optional; /** * Builds instances of {@link Options} for {@link NoPlayer#loadVideo(Uri, Options)}. */ public class OptionsBuilder { private static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000; private static final int DEFAULT_MAX_INITIAL_BITRATE = 800000; private static final int DEFAULT_MAX_VIDEO_BITRATE = Integer.MAX_VALUE; private ContentType contentType = ContentType.H264; private int minDurationBeforeQualityIncreaseInMillis = DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS; private int maxInitialBitrate = DEFAULT_MAX_INITIAL_BITRATE; private int maxVideoBitrate = DEFAULT_MAX_VIDEO_BITRATE; private Optional initialPositionInMillis = Optional.absent(); /** * Sets {@link OptionsBuilder} to build {@link Options} with a given {@link ContentType}. * This content type is passed to the underlying Player. * * @param contentType format of the content. * @return {@link OptionsBuilder}. */ public OptionsBuilder withContentType(ContentType contentType) { this.contentType = contentType; return this; } /** * Sets {@link OptionsBuilder} to build {@link Options} so that the {@link NoPlayer} * switches to a higher quality video track after given time. * * @param minDurationInMillis time elapsed before switching to a higher quality video track. * @return {@link OptionsBuilder}. */ public OptionsBuilder withMinDurationBeforeQualityIncreaseInMillis(int minDurationInMillis) { this.minDurationBeforeQualityIncreaseInMillis = minDurationInMillis; return this; } /** * Sets {@link OptionsBuilder} to build {@link Options} with given maximum initial bitrate in order to * control what is the quality with which {@link NoPlayer} starts the playback. Setting a higher value * allows the player to choose a higher quality video track at the beginning. * * @param maxInitialBitrate maximum bitrate that limits the initial track selection. * @return {@link OptionsBuilder}. */ public OptionsBuilder withMaxInitialBitrate(int maxInitialBitrate) { this.maxInitialBitrate = maxInitialBitrate; return this; } /** * Sets {@link OptionsBuilder} to build {@link Options} with given maximum video bitrate in order to * control what is the maximum video quality with which {@link NoPlayer} starts the playback. Setting a higher value * allows the player to choose a higher quality video track. * * @param maxVideoBitrate maximum bitrate that limits the initial track selection. * @return {@link OptionsBuilder} */ public OptionsBuilder withMaxVideoBitrate(int maxVideoBitrate) { this.maxVideoBitrate = maxVideoBitrate; return this; } /** * Sets {@link OptionsBuilder} to build {@link Options} with given initial position in millis in order * to specify the start position of the content that will be played. Omitting to set this will start * playback at the beginning of the content. * * @param initialPositionInMillis position that the content should begin playback at. * @return {@link OptionsBuilder}. */ public OptionsBuilder withInitialPositionInMillis(long initialPositionInMillis) { this.initialPositionInMillis = Optional.of(initialPositionInMillis); return this; } /** * Builds a new {@link Options} instance. * * @return a {@link Options} instance. */ public Options build() { return new Options( contentType, minDurationBeforeQualityIncreaseInMillis, maxInitialBitrate, maxVideoBitrate, initialPositionInMillis ); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/PlayerBuilder.java ================================================ package com.novoda.noplayer; import android.content.Context; import android.os.Handler; import android.os.Looper; import com.novoda.noplayer.drm.DownloadedModularDrm; import com.novoda.noplayer.drm.DrmHandler; import com.novoda.noplayer.drm.DrmType; import com.novoda.noplayer.drm.StreamingModularDrm; import com.novoda.noplayer.internal.drm.provision.ProvisionExecutorCreator; import com.novoda.noplayer.internal.exoplayer.NoPlayerExoPlayerCreator; import com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreatorFactory; import com.novoda.noplayer.internal.mediaplayer.NoPlayerMediaPlayerCreator; import com.novoda.noplayer.internal.utils.AndroidDeviceVersion; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * Builds instances of {@link NoPlayer} for given configurations. */ public class PlayerBuilder { private DrmType drmType = DrmType.NONE; private DrmHandler drmHandler = DrmHandler.NO_DRM; private List prioritizedPlayerTypes = Arrays.asList(PlayerType.EXO_PLAYER, PlayerType.MEDIA_PLAYER); private boolean downgradeSecureDecoder; /* initialised to false by default */ private boolean allowCrossProtocolRedirects; /* initialised to false by default */ private String userAgent = "user-agent"; /** * Sets {@link PlayerBuilder} to build a {@link NoPlayer} which supports Widevine classic DRM. * * @return {@link PlayerBuilder} * @see NoPlayer */ public PlayerBuilder withWidevineClassicDrm() { return withDrm(DrmType.WIDEVINE_CLASSIC, DrmHandler.NO_DRM); } /** * Sets {@link PlayerBuilder} to build a {@link NoPlayer} which supports Widevine modular streaming DRM. * * @param streamingModularDrm Implementation of {@link StreamingModularDrm}. * @return {@link PlayerBuilder} * @see NoPlayer */ public PlayerBuilder withWidevineModularStreamingDrm(StreamingModularDrm streamingModularDrm) { return withDrm(DrmType.WIDEVINE_MODULAR_STREAM, streamingModularDrm); } /** * Sets {@link PlayerBuilder} to build a {@link NoPlayer} which supports Widevine modular download DRM. * * @param downloadedModularDrm Implementation of {@link DownloadedModularDrm}. * @return {@link PlayerBuilder} * @see NoPlayer */ public PlayerBuilder withWidevineModularDownloadDrm(DownloadedModularDrm downloadedModularDrm) { return withDrm(DrmType.WIDEVINE_MODULAR_DOWNLOAD, downloadedModularDrm); } /** * Sets {@link PlayerBuilder} to build a {@link NoPlayer} which supports the specified parameters. * * @param drmType {@link DrmType} * @param drmHandler {@link DrmHandler} * @return {@link PlayerBuilder} * @see NoPlayer */ public PlayerBuilder withDrm(DrmType drmType, DrmHandler drmHandler) { this.drmType = drmType; this.drmHandler = drmHandler; return this; } /** * Sets {@link PlayerBuilder} to build a {@link NoPlayer} which will prioritise the underlying player when * multiple underlying players share the same features. * * @param playerType First {@link PlayerType} with the highest priority. * @param playerTypes Remaining {@link PlayerType} in order of priority. * @return {@link PlayerBuilder} * @see NoPlayer */ public PlayerBuilder withPriority(PlayerType playerType, PlayerType... playerTypes) { List types = new ArrayList<>(); types.add(playerType); types.addAll(Arrays.asList(playerTypes)); prioritizedPlayerTypes = types; return this; } /** * Forces secure decoder selection to be ignored in favour of using an insecure decoder. * e.g. Forcing an L3 stream to play with an insecure decoder instead of a secure decoder by default. * * @return {@link PlayerBuilder} */ public PlayerBuilder withDowngradedSecureDecoder() { downgradeSecureDecoder = true; return this; } /** * @param userAgent The application's user-agent value * @return {@link PlayerBuilder} */ public PlayerBuilder withUserAgent(String userAgent) { this.userAgent = userAgent; return this; } /** * Network connections will be allowed to perform redirects between HTTP and HTTPS protocols * @return {@link PlayerBuilder} */ public PlayerBuilder allowCrossProtocolRedirects() { allowCrossProtocolRedirects = true; return this; } /** * Builds a new {@link NoPlayer} instance. * * @param context The {@link Context} associated with the player. * @return a {@link NoPlayer} instance. * @throws UnableToCreatePlayerException thrown when the configuration is not supported and there is no way to recover. * @see NoPlayer */ public NoPlayer build(Context context) throws UnableToCreatePlayerException { Context applicationContext = context.getApplicationContext(); Handler handler = new Handler(Looper.getMainLooper()); ProvisionExecutorCreator provisionExecutorCreator = new ProvisionExecutorCreator(); DrmSessionCreatorFactory drmSessionCreatorFactory = new DrmSessionCreatorFactory( AndroidDeviceVersion.newInstance(), provisionExecutorCreator, handler ); NoPlayerCreator noPlayerCreator = new NoPlayerCreator( applicationContext, prioritizedPlayerTypes, NoPlayerExoPlayerCreator.newInstance(userAgent, handler), NoPlayerMediaPlayerCreator.newInstance(handler), drmSessionCreatorFactory ); return noPlayerCreator.create(drmType, drmHandler, downgradeSecureDecoder, allowCrossProtocolRedirects); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/PlayerCapabilities.java ================================================ package com.novoda.noplayer; import com.novoda.noplayer.drm.DrmType; interface PlayerCapabilities { boolean supports(DrmType drmType); } ================================================ FILE: core/src/main/java/com/novoda/noplayer/PlayerErrorType.java ================================================ package com.novoda.noplayer; public enum PlayerErrorType { SOURCE, CONNECTIVITY, DRM, CONTENT_DECRYPTION, DEVICE_MEDIA_CAPABILITIES, RENDERER_DECODER, UNEXPECTED, UNKNOWN } ================================================ FILE: core/src/main/java/com/novoda/noplayer/PlayerInformation.java ================================================ package com.novoda.noplayer; public interface PlayerInformation { PlayerType getPlayerType(); String getVersion(); String getName(); } ================================================ FILE: core/src/main/java/com/novoda/noplayer/PlayerState.java ================================================ package com.novoda.noplayer; public interface PlayerState { boolean isPlaying(); int videoWidth(); int videoHeight(); long playheadPositionInMillis(); long mediaDurationInMillis(); int bufferPercentage(); } ================================================ FILE: core/src/main/java/com/novoda/noplayer/PlayerSurfaceHolder.java ================================================ package com.novoda.noplayer; import android.support.annotation.Nullable; import android.view.SurfaceView; import android.view.TextureView; import com.google.android.exoplayer2.Player; public class PlayerSurfaceHolder { @Nullable private final SurfaceView surfaceView; @Nullable private final TextureView textureView; private final PlayerViewSurfaceHolder surfaceHolder; public static PlayerSurfaceHolder create(SurfaceView surfaceView) { PlayerViewSurfaceHolder surfaceHolder = new PlayerViewSurfaceHolder(); surfaceView.getHolder().addCallback(surfaceHolder); return new PlayerSurfaceHolder(surfaceView, null, surfaceHolder); } public static PlayerSurfaceHolder create(TextureView textureView) { PlayerViewSurfaceHolder surfaceHolder = new PlayerViewSurfaceHolder(); textureView.setSurfaceTextureListener(surfaceHolder); return new PlayerSurfaceHolder(null, textureView, surfaceHolder); } PlayerSurfaceHolder(@Nullable SurfaceView surfaceView, @Nullable TextureView textureView, PlayerViewSurfaceHolder surfaceHolder) { this.surfaceView = surfaceView; this.textureView = textureView; this.surfaceHolder = surfaceHolder; } public SurfaceRequester getSurfaceRequester() { return surfaceHolder; } public void attach(Player.VideoComponent videoPlayer) { if (containsSurfaceView()) { videoPlayer.setVideoSurfaceView(surfaceView); } else if (containsTextureView()) { videoPlayer.setVideoTextureView(textureView); } else { throw new IllegalArgumentException("Surface container does not contain any of the expected views"); } } private boolean containsSurfaceView() { return surfaceView != null; } private boolean containsTextureView() { return textureView != null; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/PlayerType.java ================================================ package com.novoda.noplayer; import com.novoda.noplayer.drm.DrmType; public enum PlayerType { MEDIA_PLAYER(new AndroidMediaPlayerCapabilities()), EXO_PLAYER(new ExoPlayerCapabilities()); private final PlayerCapabilities playerCapabilities; PlayerType(PlayerCapabilities playerCapabilities) { this.playerCapabilities = playerCapabilities; } boolean supports(DrmType drmType) { return playerCapabilities.supports(drmType); } public static PlayerType from(String rawPlayerType) { for (PlayerType playerType : values()) { if (playerType.name().equalsIgnoreCase(rawPlayerType)) { return playerType; } } throw new UnknownPlayerTypeException(rawPlayerType); } static class UnknownPlayerTypeException extends RuntimeException { UnknownPlayerTypeException(String rawPlayerType) { super("Can't create PlayerType from : " + rawPlayerType); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/PlayerView.java ================================================ package com.novoda.noplayer; import android.view.View; import com.novoda.noplayer.model.TextCues; public interface PlayerView { View getContainerView(); PlayerSurfaceHolder getPlayerSurfaceHolder(); NoPlayer.VideoSizeChangedListener getVideoSizeChangedListener(); NoPlayer.StateChangedListener getStateChangedListener(); void showSubtitles(); void hideSubtitles(); void setSubtitleCue(TextCues textCues); } ================================================ FILE: core/src/main/java/com/novoda/noplayer/PlayerViewSurfaceHolder.java ================================================ package com.novoda.noplayer; import android.graphics.SurfaceTexture; import android.support.annotation.Nullable; import android.view.Surface; import android.view.SurfaceHolder; import android.view.TextureView; import com.novoda.noplayer.model.Either; import java.util.ArrayList; import java.util.List; class PlayerViewSurfaceHolder implements SurfaceHolder.Callback, TextureView.SurfaceTextureListener, SurfaceRequester { private final List callbacks = new ArrayList<>(); @Nullable private Either eitherSurface; @Override public void surfaceCreated(SurfaceHolder surfaceHolder) { this.eitherSurface = Either.right(surfaceHolder); notifyListeners(eitherSurface); callbacks.clear(); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // do nothing } @Override public void surfaceDestroyed(SurfaceHolder holder) { setSurfaceNotReady(); callbacks.clear(); } @Override public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { this.eitherSurface = Either.left(new Surface(surfaceTexture)); notifyListeners(eitherSurface); callbacks.clear(); } private void notifyListeners(Either either) { for (Callback callback : callbacks) { callback.onSurfaceReady(either); } } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { // do nothing } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { setSurfaceNotReady(); surface.release(); callbacks.clear(); return true; } @Override public void onSurfaceTextureUpdated(SurfaceTexture surface) { // do nothing } private void setSurfaceNotReady() { eitherSurface = null; } @Override public void requestSurface(Callback callback) { if (isSurfaceReady()) { callback.onSurfaceReady(eitherSurface); } else { callbacks.add(callback); } } private boolean isSurfaceReady() { return eitherSurface != null; } @Override public void removeCallback(Callback callback) { callbacks.remove(callback); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/SubtitlePainter.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.novoda.noplayer; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Join; import android.graphics.Paint.Style; import android.graphics.Rect; import android.graphics.RectF; import android.text.Layout.Alignment; import android.text.SpannableStringBuilder; import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextUtils; import android.text.style.AbsoluteSizeSpan; import android.text.style.RelativeSizeSpan; import android.util.DisplayMetrics; import android.util.Log; import com.google.android.exoplayer2.text.CaptionStyleCompat; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.util.Util; import com.novoda.noplayer.model.NoPlayerCue; // Adopted code, could use some refactoring but it's a complex job @SuppressWarnings({"PMD.GodClass", "PMD.CyclomaticComplexity", "PMD.StdCyclomaticComplexity", "PMD.ModifiedCyclomaticComplexity"}) final class SubtitlePainter { private static final String TAG = "SubtitlePainter"; private static final float INNER_PADDING_RATIO = 0.125f; private static final float ROUNDING_HALF_PIXEL = 0.5f; private static final float TWO_DP = 2f; private static final double FLOAT_COMPARISON_EPSILON = .0000001; private final RectF lineBounds = new RectF(); // Styled dimensions. private final float cornerRadius; private final float outlineWidth; private final float shadowRadius; private final float shadowOffset; private final float spacingMult; private final float spacingAdd; private final TextPaint textPaint; private final Paint paint; // Previous input variables. private CharSequence cueText; private Alignment cueTextAlignment; private Bitmap cueBitmap; private float cueLine; @Cue.LineType private int cueLineType; @Cue.AnchorType private int cueLineAnchor; private float cuePosition; @Cue.AnchorType private int cuePositionAnchor; private float cueSize; private float cueBitmapHeight; private boolean applyEmbeddedStyles; private boolean applyEmbeddedFontSizes; private int foregroundColor; private int backgroundColor; private int windowColor; private int edgeColor; @CaptionStyleCompat.EdgeType private int edgeType; private float textSizePx; private float bottomPaddingFraction; private int parentLeft; private int parentTop; private int parentRight; private int parentBottom; // Derived drawing variables. private StaticLayout textLayout; private int textLeft; private int textTop; private int textPaddingX; private Rect bitmapRect; @SuppressWarnings("ResourceType") // We're hacking `spacingMult = styledAttributes.getFloat` SubtitlePainter(Context context) { int[] viewAttr = {android.R.attr.lineSpacingExtra, android.R.attr.lineSpacingMultiplier}; TypedArray styledAttributes = context.obtainStyledAttributes(null, viewAttr, 0, 0); spacingAdd = styledAttributes.getDimensionPixelSize(0, 0); spacingMult = styledAttributes.getFloat(1, 1); styledAttributes.recycle(); Resources resources = context.getResources(); DisplayMetrics displayMetrics = resources.getDisplayMetrics(); int twoDpInPx = Math.round((TWO_DP * displayMetrics.densityDpi) / DisplayMetrics.DENSITY_DEFAULT); cornerRadius = twoDpInPx; outlineWidth = twoDpInPx; shadowRadius = twoDpInPx; shadowOffset = twoDpInPx; textPaint = new TextPaint(); textPaint.setAntiAlias(true); textPaint.setSubpixelText(true); paint = new Paint(); paint.setAntiAlias(true); paint.setStyle(Style.FILL); } @SuppressWarnings({"checkstyle:ParameterNumber", "PMD.ExcessiveParameterList"}) // TODO group parameters into classes void draw(NoPlayerCue cue, boolean applyEmbeddedStyles, boolean applyEmbeddedFontSizes, float textSizePx, float bottomPaddingFraction, Canvas canvas, int cueBoxLeft, int cueBoxTop, int cueBoxRight, int cueBoxBottom) { boolean isTextCue = cue.bitmap() == null; int windowColor = Color.BLACK; if (isTextCue) { if (TextUtils.isEmpty(cue.text())) { // Nothing to draw. return; } windowColor = (cue.windowColorSet() && applyEmbeddedStyles) ? cue.windowColor() : Color.TRANSPARENT; } if (nothingHasChanged( cue, applyEmbeddedStyles, applyEmbeddedFontSizes, textSizePx, bottomPaddingFraction, cueBoxLeft, cueBoxTop, cueBoxRight, cueBoxBottom, windowColor)) { // We can use the cached layout. drawLayout(canvas, isTextCue); return; } this.cueText = cue.text(); this.cueTextAlignment = cue.textAlignment(); this.cueBitmap = cue.bitmap(); this.cueLine = cue.line(); this.cueLineType = cue.lineType(); this.cueLineAnchor = cue.lineAnchor(); this.cuePosition = cue.position(); this.cuePositionAnchor = cue.positionAnchor(); this.cueSize = cue.size(); this.cueBitmapHeight = cue.bitmapHeight(); this.applyEmbeddedStyles = applyEmbeddedStyles; this.applyEmbeddedFontSizes = applyEmbeddedFontSizes; this.foregroundColor = Color.WHITE; this.backgroundColor = Color.BLACK; this.windowColor = windowColor; this.edgeType = 0; this.edgeColor = Color.WHITE; textPaint.setTypeface(null); this.textSizePx = textSizePx; this.bottomPaddingFraction = bottomPaddingFraction; this.parentLeft = cueBoxLeft; this.parentTop = cueBoxTop; this.parentRight = cueBoxRight; this.parentBottom = cueBoxBottom; if (isTextCue) { setupTextLayout(); } else { setupBitmapLayout(); } drawLayout(canvas, isTextCue); } @SuppressWarnings({"checkstyle:ParameterNumber", "PMD.ExcessiveParameterList"}) // TODO group parameters into classes private boolean nothingHasChanged(NoPlayerCue cue, boolean applyEmbeddedStyles, boolean applyEmbeddedFontSizes, float textSizePx, float bottomPaddingFraction, int cueBoxLeft, int cueBoxTop, int cueBoxRight, int cueBoxBottom, int windowColor) { return areCharSequencesEqual(cueText, cue.text()) && Util.areEqual(cueTextAlignment, cue.textAlignment()) && cueBitmap == cue.bitmap() && cueLine == cue.line() && cueLineType == cue.lineType() && Util.areEqual(cueLineAnchor, cue.lineAnchor()) && cuePosition == cue.position() && Util.areEqual(cuePositionAnchor, cue.positionAnchor()) && cueSize == cue.size() && cueBitmapHeight == cue.bitmapHeight() && this.applyEmbeddedStyles == applyEmbeddedStyles && this.applyEmbeddedFontSizes == applyEmbeddedFontSizes && foregroundColor == Color.WHITE && backgroundColor == Color.BLACK && this.windowColor == windowColor && edgeType == 0 && edgeColor == Color.WHITE && Util.areEqual(textPaint.getTypeface(), null) && this.textSizePx == textSizePx && this.bottomPaddingFraction == bottomPaddingFraction && parentLeft == cueBoxLeft && parentTop == cueBoxTop && parentRight == cueBoxRight && parentBottom == cueBoxBottom; } @SuppressWarnings({"PMD.ExcessiveMethodLength", "PMD.NPathComplexity" }) // TODO break this method up private void setupTextLayout() { int parentWidth = parentRight - parentLeft; textPaint.setTextSize(textSizePx); int textPaddingX = (int) (textSizePx * INNER_PADDING_RATIO + ROUNDING_HALF_PIXEL); int availableWidth = parentWidth - textPaddingX * 2; if (isCueDimensionSet(cueSize)) { availableWidth *= cueSize; } if (availableWidth <= 0) { Log.w(TAG, "Skipped drawing subtitle cue (insufficient space)"); return; } // Remove embedded styling or font size if requested. CharSequence cueText; if (applyEmbeddedFontSizes && applyEmbeddedStyles) { cueText = this.cueText; } else if (!applyEmbeddedStyles) { cueText = this.cueText.toString(); // Equivalent to erasing all spans. } else { SpannableStringBuilder newCueText = new SpannableStringBuilder(this.cueText); int cueLength = newCueText.length(); AbsoluteSizeSpan[] absSpans = newCueText.getSpans(0, cueLength, AbsoluteSizeSpan.class); RelativeSizeSpan[] relSpans = newCueText.getSpans(0, cueLength, RelativeSizeSpan.class); for (AbsoluteSizeSpan absSpan : absSpans) { newCueText.removeSpan(absSpan); } for (RelativeSizeSpan relSpan : relSpans) { newCueText.removeSpan(relSpan); } cueText = newCueText; } Alignment textAlignment = cueTextAlignment == null ? Alignment.ALIGN_CENTER : cueTextAlignment; textLayout = new StaticLayout(cueText, textPaint, availableWidth, textAlignment, spacingMult, spacingAdd, true); int textWidth = 0; int lineCount = textLayout.getLineCount(); for (int i = 0; i < lineCount; i++) { textWidth = Math.max((int) Math.ceil(textLayout.getLineWidth(i)), textWidth); } if (isCueDimensionSet(cueSize) && textWidth < availableWidth) { textWidth = availableWidth; } textWidth += textPaddingX * 2; int textLeft; int textRight; if (isCueDimensionSet(cuePosition)) { int anchorPosition = Math.round(parentWidth * cuePosition) + parentLeft; textLeft = cuePositionAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textWidth : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorPosition * 2 - textWidth) / 2 : anchorPosition; textLeft = Math.max(textLeft, parentLeft); textRight = Math.min(textLeft + textWidth, parentRight); } else { textLeft = (parentWidth - textWidth) / 2; textRight = textLeft + textWidth; } textWidth = textRight - textLeft; if (textWidth <= 0) { Log.w(TAG, "Skipped drawing subtitle cue (invalid horizontal positioning)"); return; } int parentHeight = parentBottom - parentTop; int textHeight = textLayout.getHeight(); int textTop; if (isCueDimensionSet(cueLine)) { int anchorPosition; if (cueLineType == Cue.LINE_TYPE_FRACTION) { anchorPosition = Math.round(parentHeight * cueLine) + parentTop; } else { // cueLineType == Cue.LINE_TYPE_NUMBER int firstLineHeight = textLayout.getLineBottom(0) - textLayout.getLineTop(0); if (cueLine >= 0) { anchorPosition = Math.round(cueLine * firstLineHeight) + parentTop; } else { anchorPosition = Math.round((cueLine + 1) * firstLineHeight) + parentBottom; } } textTop = cueLineAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textHeight : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorPosition * 2 - textHeight) / 2 : anchorPosition; if (textTop + textHeight > parentBottom) { textTop = parentBottom - textHeight; } else if (textTop < parentTop) { textTop = parentTop; } } else { textTop = parentBottom - textHeight - (int) (parentHeight * bottomPaddingFraction); } // Update the derived drawing variables. this.textLayout = new StaticLayout(cueText, textPaint, textWidth, textAlignment, spacingMult, spacingAdd, true); this.textLeft = textLeft; this.textTop = textTop; this.textPaddingX = textPaddingX; } @SuppressWarnings("PMD.NPathComplexity") // TODO break this method up private void setupBitmapLayout() { int parentWidth = parentRight - parentLeft; int parentHeight = parentBottom - parentTop; float anchorX = parentLeft + (parentWidth * cuePosition); float anchorY = parentTop + (parentHeight * cueLine); int width = Math.round(parentWidth * cueSize); int height = isCueDimensionSet(cueBitmapHeight) ? Math.round(parentHeight * cueBitmapHeight) : Math.round(width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth())); int x = Math.round(cueLineAnchor == Cue.ANCHOR_TYPE_END ? (anchorX - width) : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2f)) : anchorX); int y = Math.round(cuePositionAnchor == Cue.ANCHOR_TYPE_END ? (anchorY - height) : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorY - (height / 2f)) : anchorY); bitmapRect = new Rect(x, y, x + width, y + height); } private boolean isCueDimensionSet(float cueDimension) { return Math.abs(cueDimension - Cue.DIMEN_UNSET) > FLOAT_COMPARISON_EPSILON; } private void drawLayout(Canvas canvas, boolean isTextCue) { if (isTextCue) { drawTextLayout(canvas); } else { drawBitmapLayout(canvas); } } @SuppressWarnings("PMD.NPathComplexity") // TODO break this method up private void drawTextLayout(Canvas canvas) { StaticLayout layout = textLayout; if (layout == null) { // Nothing to draw. return; } int saveCount = canvas.save(); canvas.translate(textLeft, textTop); if (Color.alpha(windowColor) > 0) { paint.setColor(windowColor); canvas.drawRect(-textPaddingX, 0, layout.getWidth() + textPaddingX, layout.getHeight(), paint); } if (Color.alpha(backgroundColor) > 0) { paint.setColor(backgroundColor); float previousBottom = layout.getLineTop(0); int lineCount = layout.getLineCount(); for (int i = 0; i < lineCount; i++) { lineBounds.left = layout.getLineLeft(i) - textPaddingX; lineBounds.right = layout.getLineRight(i) + textPaddingX; lineBounds.top = previousBottom; lineBounds.bottom = layout.getLineBottom(i); previousBottom = lineBounds.bottom; canvas.drawRoundRect(lineBounds, cornerRadius, cornerRadius, paint); } } if (edgeType == CaptionStyleCompat.EDGE_TYPE_OUTLINE) { textPaint.setStrokeJoin(Join.ROUND); textPaint.setStrokeWidth(outlineWidth); textPaint.setColor(edgeColor); textPaint.setStyle(Style.FILL_AND_STROKE); layout.draw(canvas); } else if (edgeType == CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW) { textPaint.setShadowLayer(shadowRadius, shadowOffset, shadowOffset, edgeColor); } else if (edgeType == CaptionStyleCompat.EDGE_TYPE_RAISED || edgeType == CaptionStyleCompat.EDGE_TYPE_DEPRESSED) { boolean raised = edgeType == CaptionStyleCompat.EDGE_TYPE_RAISED; int colorUp = raised ? Color.WHITE : edgeColor; int colorDown = raised ? edgeColor : Color.WHITE; float offset = shadowRadius / 2; textPaint.setColor(foregroundColor); textPaint.setStyle(Style.FILL); textPaint.setShadowLayer(shadowRadius, -offset, -offset, colorUp); layout.draw(canvas); textPaint.setShadowLayer(shadowRadius, offset, offset, colorDown); } textPaint.setColor(foregroundColor); textPaint.setStyle(Style.FILL); layout.draw(canvas); textPaint.setShadowLayer(0, 0, 0, 0); canvas.restoreToCount(saveCount); } private void drawBitmapLayout(Canvas canvas) { canvas.drawBitmap(cueBitmap, null, bitmapRect, null); } /** * This method is used instead of {@link TextUtils#equals(CharSequence, CharSequence)} because the * latter only checks the text of each sequence, and does not check for equality of styling that * may be embedded within the {@link CharSequence}s. */ @SuppressWarnings("PMD.CompareObjectsWithEquals") // We do, but we first try to shortcut by comparing references private static boolean areCharSequencesEqual(CharSequence first, CharSequence second) { // Some CharSequence implementations don't perform a cheap referential equality check in their // equals methods, so we perform one explicitly here. return first == second || (first != null && first.equals(second)); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/SubtitleView.java ================================================ package com.novoda.noplayer; import android.content.Context; import android.graphics.Canvas; import android.util.AttributeSet; import android.view.View; import com.novoda.noplayer.model.TextCues; import java.util.ArrayList; import java.util.List; public final class SubtitleView extends View { private static final float DEFAULT_TEXT_SIZE_FRACTION = 0.0533f; private static final float DEFAULT_BOTTOM_PADDING_FRACTION = 0.08f; private static final boolean APPLY_EMBEDDED_STYLES = true; private static final boolean APPLY_EMBEDDED_FONT_STYLES = true; private static final int ZERO_PIXELS = 0; private final List painters; private TextCues textCues; public SubtitleView(Context context, AttributeSet attrs) { super(context, attrs); painters = new ArrayList<>(); } public void setCues(TextCues textCues) { if (textCues.equals(this.textCues)) { return; } this.textCues = textCues; int cueCount = textCues.size(); while (painters.size() < cueCount) { painters.add(new SubtitlePainter(getContext())); } invalidate(); } @Override public void dispatchDraw(Canvas canvas) { if (textCues == null || textCues.isEmpty()) { return; } int rawTop = getTop(); int rawBottom = getBottom(); int left = getLeft() + getPaddingLeft(); int top = rawTop + getPaddingTop(); int right = getRight() + getPaddingRight(); int bottom = rawBottom - getPaddingBottom(); if (bottom <= top || right <= left) { return; } float textSizeInPixels = DEFAULT_TEXT_SIZE_FRACTION * (bottom - top); if (textSizeInPixels <= ZERO_PIXELS) { return; } int cueCount = textCues.size(); for (int i = 0; i < cueCount; i++) { painters.get(i).draw( textCues.get(i), APPLY_EMBEDDED_STYLES, APPLY_EMBEDDED_FONT_STYLES, textSizeInPixels, DEFAULT_BOTTOM_PADDING_FRACTION, canvas, left, top, right, bottom ); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/SurfaceRequester.java ================================================ package com.novoda.noplayer; import android.view.Surface; import android.view.SurfaceHolder; import com.novoda.noplayer.model.Either; public interface SurfaceRequester { void requestSurface(Callback callback); void removeCallback(Callback callback); interface Callback { void onSurfaceReady(Either surface); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/UnableToCreatePlayerException.java ================================================ package com.novoda.noplayer; import com.novoda.noplayer.drm.DrmType; import com.novoda.noplayer.internal.utils.AndroidDeviceVersion; public class UnableToCreatePlayerException extends RuntimeException { static UnableToCreatePlayerException unhandledDrmType(DrmType drmType) { return new UnableToCreatePlayerException("Unhandled DrmType: " + drmType); } static UnableToCreatePlayerException unhandledPlayerType(PlayerType playerType) { return new UnableToCreatePlayerException("Unhandled player type: " + playerType.name()); } public static UnableToCreatePlayerException deviceDoesNotMeetTargetApiException(DrmType drmType, int targetApiLevel, AndroidDeviceVersion actualApiLevel) { return new UnableToCreatePlayerException( "Device must be target: " + targetApiLevel + " but was: " + actualApiLevel.sdkInt() + " for DRM type: " + drmType.name() ); } UnableToCreatePlayerException(Throwable cause) { super(cause); } private UnableToCreatePlayerException(String reason) { super(reason); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/drm/DownloadedModularDrm.java ================================================ package com.novoda.noplayer.drm; import com.novoda.noplayer.model.KeySetId; public interface DownloadedModularDrm extends DrmHandler { KeySetId getKeySetId(); } ================================================ FILE: core/src/main/java/com/novoda/noplayer/drm/DrmHandler.java ================================================ package com.novoda.noplayer.drm; @SuppressWarnings({ "checkstyle:interfaceistype", "PMD.AvoidConstantsInterface", "PMD.ConstantsInInterface" }) // This is to allow for multiple different types of DRM public interface DrmHandler { DrmHandler NO_DRM = new DrmHandler() { }; } ================================================ FILE: core/src/main/java/com/novoda/noplayer/drm/DrmType.java ================================================ package com.novoda.noplayer.drm; public enum DrmType { NONE, WIDEVINE_CLASSIC, WIDEVINE_MODULAR_STREAM, WIDEVINE_MODULAR_DOWNLOAD } ================================================ FILE: core/src/main/java/com/novoda/noplayer/drm/ModularDrmKeyRequest.java ================================================ package com.novoda.noplayer.drm; import java.util.Arrays; public class ModularDrmKeyRequest { private final String url; private final byte[] data; public ModularDrmKeyRequest(String url, byte[] data) { this.url = url; this.data = Arrays.copyOf(data, data.length); } public String url() { return url; } public byte[] data() { return Arrays.copyOf(data, data.length); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } ModularDrmKeyRequest that = (ModularDrmKeyRequest) o; if (url != null ? !url.equals(that.url) : that.url != null) { return false; } return Arrays.equals(data, that.data); } @Override public int hashCode() { int result = url != null ? url.hashCode() : 0; result = 31 * result + Arrays.hashCode(data); return result; } @Override public String toString() { return "ModularDrmKeyRequest{" + "url='" + url + '\'' + ", data=" + Arrays.toString(data) + '}'; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/drm/ModularDrmProvisionRequest.java ================================================ package com.novoda.noplayer.drm; import java.util.Arrays; public class ModularDrmProvisionRequest { private final String url; private final byte[] data; public ModularDrmProvisionRequest(String url, byte[] data) { this.url = url; this.data = Arrays.copyOf(data, data.length); } public String url() { return url; } public byte[] data() { return Arrays.copyOf(data, data.length); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } ModularDrmProvisionRequest that = (ModularDrmProvisionRequest) o; if (url != null ? !url.equals(that.url) : that.url != null) { return false; } return Arrays.equals(data, that.data); } @Override public int hashCode() { int result = url != null ? url.hashCode() : 0; result = 31 * result + Arrays.hashCode(data); return result; } @Override public String toString() { return "ModularDrmProvisionRequest{" + "url='" + url + '\'' + ", data=" + Arrays.toString(data) + '}'; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/drm/StreamingModularDrm.java ================================================ package com.novoda.noplayer.drm; public interface StreamingModularDrm extends DrmHandler { byte[] executeKeyRequest(ModularDrmKeyRequest request) throws DrmRequestException; final class DrmRequestException extends Exception { public static DrmRequestException from(Exception e) { return new DrmRequestException("Drm http request failed : " + e.getMessage(), e); } public static DrmRequestException invalidHttpCode(int code, String body) { return new DrmRequestException("Unexpected response HTTP code: " + code + " | " + body); } private DrmRequestException(String detailMessage) { super(detailMessage); } private DrmRequestException(String message, Throwable cause) { super(message, cause); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/external/exoplayer/text/webvtt/CssParser.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.novoda.noplayer.external.exoplayer.text.webvtt; import android.text.TextUtils; import com.google.android.exoplayer2.text.webvtt.WebvttCssStyle; import com.google.android.exoplayer2.util.ColorParser; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.Arrays; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Provides a CSS parser for STYLE blocks in Webvtt files. Supports only a subset of the CSS * features. */ /* package */ final class CssParser { private static final String PROPERTY_BGCOLOR = "background-color"; private static final String PROPERTY_FONT_FAMILY = "font-family"; private static final String PROPERTY_FONT_WEIGHT = "font-weight"; private static final String PROPERTY_TEXT_DECORATION = "text-decoration"; private static final String VALUE_BOLD = "bold"; private static final String VALUE_UNDERLINE = "underline"; private static final String BLOCK_START = "{"; private static final String BLOCK_END = "}"; private static final String PROPERTY_FONT_STYLE = "font-style"; private static final String VALUE_ITALIC = "italic"; private static final Pattern VOICE_NAME_PATTERN = Pattern.compile("\\[voice=\"([^\"]*)\"\\]"); // Temporary utility data structures. private final ParsableByteArray styleInput; private final StringBuilder stringBuilder; public CssParser() { styleInput = new ParsableByteArray(); stringBuilder = new StringBuilder(); } /** * Takes a CSS style block and consumes up to the first empty line found. Attempts to parse the * contents of the style block and returns a {@link WebvttCssStyle} instance if successful, or * {@code null} otherwise. * * @param input The input from which the style block should be read. * @return A {@link WebvttCssStyle} that represents the parsed block. */ public WebvttCssStyle parseBlock(ParsableByteArray input) { stringBuilder.setLength(0); int initialInputPosition = input.getPosition(); skipStyleBlock(input); styleInput.reset(input.data, input.getPosition()); styleInput.setPosition(initialInputPosition); String selector = parseSelector(styleInput, stringBuilder); if (selector == null || !BLOCK_START.equals(parseNextToken(styleInput, stringBuilder))) { return null; } WebvttCssStyle style = new WebvttCssStyle(); applySelectorToStyle(style, selector); String token = null; boolean blockEndFound = false; while (!blockEndFound) { int position = styleInput.getPosition(); token = parseNextToken(styleInput, stringBuilder); blockEndFound = token == null || BLOCK_END.equals(token); if (!blockEndFound) { styleInput.setPosition(position); parseStyleDeclaration(styleInput, style, stringBuilder); } } return BLOCK_END.equals(token) ? style : null; // Check that the style block ended correctly. } /** * Returns a string containing the selector. The input is expected to have the form * {@code ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional. * * @param input From which the selector is obtained. * @return A string containing the target, empty string if the selector is universal * (targets all cues) or null if an error was encountered. */ private static String parseSelector(ParsableByteArray input, StringBuilder stringBuilder) { skipWhitespaceAndComments(input); if (input.bytesLeft() < 5) { return null; } String cueSelector = input.readString(5); if (!"::cue".equals(cueSelector)) { return null; } int position = input.getPosition(); String token = parseNextToken(input, stringBuilder); if (token == null) { return null; } if (BLOCK_START.equals(token)) { input.setPosition(position); return ""; } String target = null; if ("(".equals(token)) { target = readCueTarget(input); } token = parseNextToken(input, stringBuilder); if (!")".equals(token) || token == null) { return null; } return target; } /** * Reads the contents of ::cue() and returns it as a string. */ private static String readCueTarget(ParsableByteArray input) { int position = input.getPosition(); int limit = input.limit(); boolean cueTargetEndFound = false; while (position < limit && !cueTargetEndFound) { char c = (char) input.data[position++]; cueTargetEndFound = c == ')'; } return input.readString(--position - input.getPosition()).trim(); // --offset to return ')' to the input. } private static void parseStyleDeclaration(ParsableByteArray input, WebvttCssStyle style, StringBuilder stringBuilder) { skipWhitespaceAndComments(input); String property = parseIdentifier(input, stringBuilder); if ("".equals(property)) { return; } if (!":".equals(parseNextToken(input, stringBuilder))) { return; } skipWhitespaceAndComments(input); String value = parsePropertyValue(input, stringBuilder); if (value == null || "".equals(value)) { return; } int position = input.getPosition(); String token = parseNextToken(input, stringBuilder); if (";".equals(token)) { // The style declaration is well formed. } else if (BLOCK_END.equals(token)) { // The style declaration is well formed and we can go on, but the closing bracket had to be // fed back. input.setPosition(position); } else { // The style declaration is not well formed. return; } // At this point we have a presumably valid declaration, we need to parse it and fill the style. if ("color".equals(property)) { style.setFontColor(ColorParser.parseCssColor(value)); } else if (PROPERTY_BGCOLOR.equals(property)) { style.setBackgroundColor(ColorParser.parseCssColor(value)); } else if (PROPERTY_TEXT_DECORATION.equals(property)) { if (VALUE_UNDERLINE.equals(value)) { style.setUnderline(true); } } else if (PROPERTY_FONT_FAMILY.equals(property)) { style.setFontFamily(value); } else if (PROPERTY_FONT_WEIGHT.equals(property)) { if (VALUE_BOLD.equals(value)) { style.setBold(true); } } else if (PROPERTY_FONT_STYLE.equals(property)) { if (VALUE_ITALIC.equals(value)) { style.setItalic(true); } } // TODO: Fill remaining supported styles. } // Visible for testing. /* package */ static void skipWhitespaceAndComments(ParsableByteArray input) { boolean skipping = true; while (input.bytesLeft() > 0 && skipping) { skipping = maybeSkipWhitespace(input) || maybeSkipComment(input); } } // Visible for testing. /* package */ static String parseNextToken(ParsableByteArray input, StringBuilder stringBuilder) { skipWhitespaceAndComments(input); if (input.bytesLeft() == 0) { return null; } String identifier = parseIdentifier(input, stringBuilder); if (!"".equals(identifier)) { return identifier; } // We found a delimiter. return "" + (char) input.readUnsignedByte(); } private static boolean maybeSkipWhitespace(ParsableByteArray input) { switch(peekCharAtPosition(input, input.getPosition())) { case '\t': case '\r': case '\n': case '\f': case ' ': input.skipBytes(1); return true; default: return false; } } // Visible for testing. /* package */ static void skipStyleBlock(ParsableByteArray input) { // The style block cannot contain empty lines, so we assume the input ends when a empty line // is found. String line; do { line = input.readLine(); } while (!TextUtils.isEmpty(line)); } private static char peekCharAtPosition(ParsableByteArray input, int position) { return (char) input.data[position]; } private static String parsePropertyValue(ParsableByteArray input, StringBuilder stringBuilder) { StringBuilder expressionBuilder = new StringBuilder(); String token; int position; boolean expressionEndFound = false; // TODO: Add support for "Strings in quotes with spaces". while (!expressionEndFound) { position = input.getPosition(); token = parseNextToken(input, stringBuilder); if (token == null) { // Syntax error. return null; } if (BLOCK_END.equals(token) || ";".equals(token)) { input.setPosition(position); expressionEndFound = true; } else { expressionBuilder.append(token); } } return expressionBuilder.toString(); } private static boolean maybeSkipComment(ParsableByteArray input) { int position = input.getPosition(); int limit = input.limit(); byte[] data = input.data; if (position + 2 <= limit && data[position++] == '/' && data[position++] == '*') { while (position + 1 < limit) { char skippedChar = (char) data[position++]; if (skippedChar == '*') { if (((char) data[position]) == '/') { position++; limit = position; } } } input.skipBytes(limit - input.getPosition()); return true; } return false; } private static String parseIdentifier(ParsableByteArray input, StringBuilder stringBuilder) { stringBuilder.setLength(0); int position = input.getPosition(); int limit = input.limit(); boolean identifierEndFound = false; while (position < limit && !identifierEndFound) { char c = (char) input.data[position]; if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '#' || c == '-' || c == '.' || c == '_') { position++; stringBuilder.append(c); } else { identifierEndFound = true; } } input.skipBytes(position - input.getPosition()); return stringBuilder.toString(); } /** * Sets the target of a {@link WebvttCssStyle} by splitting a selector of the form * {@code ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional. */ private void applySelectorToStyle(WebvttCssStyle style, String selector) { if ("".equals(selector)) { return; // Universal selector. } int voiceStartIndex = selector.indexOf('['); if (voiceStartIndex != -1) { Matcher matcher = VOICE_NAME_PATTERN.matcher(selector.substring(voiceStartIndex)); if (matcher.matches()) { style.setTargetVoice(matcher.group(1)); } selector = selector.substring(0, voiceStartIndex); } String[] classDivision = selector.split("\\."); String tagAndIdDivision = classDivision[0]; int idPrefixIndex = tagAndIdDivision.indexOf('#'); if (idPrefixIndex != -1) { style.setTargetTagName(tagAndIdDivision.substring(0, idPrefixIndex)); style.setTargetId(tagAndIdDivision.substring(idPrefixIndex + 1)); // We discard the '#'. } else { style.setTargetTagName(tagAndIdDivision); } if (classDivision.length > 1) { style.setTargetClasses(Arrays.copyOfRange(classDivision, 1, classDivision.length)); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/external/exoplayer/text/webvtt/WebvttCueParser.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.novoda.noplayer.external.exoplayer.text.webvtt; import android.graphics.Typeface; import android.support.annotation.NonNull; import android.text.Layout.Alignment; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; import android.text.style.AbsoluteSizeSpan; import android.text.style.AlignmentSpan; import android.text.style.BackgroundColorSpan; import android.text.style.ForegroundColorSpan; import android.text.style.RelativeSizeSpan; import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; import android.util.Log; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.webvtt.WebvttCssStyle; import com.google.android.exoplayer2.text.webvtt.WebvttCue; import com.google.android.exoplayer2.text.webvtt.WebvttParserUtil; import com.google.android.exoplayer2.util.ParsableByteArray; import com.novoda.noplayer.external.exoplayer.util.ColorParser; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Stack; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues) */ public final class WebvttCueParser { public static final Pattern CUE_HEADER_PATTERN = Pattern .compile("^(\\S+)\\s+-->\\s+(\\S+)(.*)?$"); private static final Pattern CUE_SETTING_PATTERN = Pattern.compile("(\\S+?):(\\S+)"); private static final char CHAR_LESS_THAN = '<'; private static final char CHAR_GREATER_THAN = '>'; private static final char CHAR_SLASH = '/'; private static final char CHAR_AMPERSAND = '&'; private static final char CHAR_SEMI_COLON = ';'; private static final char CHAR_SPACE = ' '; private static final String ENTITY_LESS_THAN = "lt"; private static final String ENTITY_GREATER_THAN = "gt"; private static final String ENTITY_AMPERSAND = "amp"; private static final String ENTITY_NON_BREAK_SPACE = "nbsp"; private static final String TAG_BOLD = "b"; private static final String TAG_ITALIC = "i"; private static final String TAG_UNDERLINE = "u"; private static final String TAG_CLASS = "c"; private static final String TAG_VOICE = "v"; private static final String TAG_LANG = "lang"; private static final int STYLE_BOLD = Typeface.BOLD; private static final int STYLE_ITALIC = Typeface.ITALIC; private static final String TAG = "WebvttCueParser"; private final StringBuilder textBuilder; public WebvttCueParser() { textBuilder = new StringBuilder(); } /** * Parses the next valid WebVTT cue in a parsable array, including timestamps, settings and text. * * @param webvttData Parsable WebVTT file data. * @param builder Builder for WebVTT Cues. * @param styles List of styles defined by the CSS style blocks preceeding the cues. * @return Whether a valid Cue was found. */ public boolean parseCue(ParsableByteArray webvttData, WebvttCue.Builder builder, List styles) { String firstLine = webvttData.readLine(); if (firstLine == null) { return false; } Matcher cueHeaderMatcher = com.google.android.exoplayer2.text.webvtt.WebvttCueParser.CUE_HEADER_PATTERN.matcher(firstLine); if (cueHeaderMatcher.matches()) { // We have found the timestamps in the first line. No id present. return parseCue(null, cueHeaderMatcher, webvttData, builder, textBuilder, styles); } // The first line is not the timestamps, but could be the cue id. String secondLine = webvttData.readLine(); if (secondLine == null) { return false; } cueHeaderMatcher = com.google.android.exoplayer2.text.webvtt.WebvttCueParser.CUE_HEADER_PATTERN.matcher(secondLine); if (cueHeaderMatcher.matches()) { // We can do the rest of the parsing, including the id. return parseCue(firstLine.trim(), cueHeaderMatcher, webvttData, builder, textBuilder, styles ); } return false; } /** * Parses a string containing a list of cue settings. * * @param cueSettingsList String containing the settings for a given cue. * @param builder The {@link WebvttCue.Builder} where incremental construction takes place. */ /* package */ static void parseCueSettingsList(String cueSettingsList, WebvttCue.Builder builder) { // Parse the cue settings list. Matcher cueSettingMatcher = CUE_SETTING_PATTERN.matcher(cueSettingsList); while (cueSettingMatcher.find()) { String name = cueSettingMatcher.group(1); String value = cueSettingMatcher.group(2); try { if ("line".equals(name)) { parseLineAttribute(value, builder); } else if ("align".equals(name)) { builder.setTextAlignment(parseTextAlignment(value)); } else if ("position".equals(name)) { parsePositionAttribute(value, builder); } else if ("size".equals(name)) { builder.setWidth(WebvttParserUtil.parsePercentage(value)); } else { Log.w(TAG, "Unknown cue setting " + name + ":" + value); } } catch (NumberFormatException e) { Log.w(TAG, "Skipping bad cue setting: " + cueSettingMatcher.group()); } } } /** * Parses the text payload of a WebVTT Cue and applies modifications on {@link WebvttCue.Builder}. * * @param id Id of the cue, {@code null} if it is not present. * @param markup The markup text to be parsed. * @param styles List of styles defined by the CSS style blocks preceeding the cues. * @param builder Output builder. */ /* package */ static void parseCueText(String id, String markup, WebvttCue.Builder builder, List styles) { SpannableStringBuilder spannedText = new SpannableStringBuilder(); Stack startTagStack = new Stack<>(); List scratchStyleMatches = new ArrayList<>(); int pos = 0; while (pos < markup.length()) { char curr = markup.charAt(pos); switch (curr) { case CHAR_LESS_THAN: if (pos + 1 >= markup.length()) { pos++; break; // avoid ArrayOutOfBoundsException } int ltPos = pos; boolean isClosingTag = markup.charAt(ltPos + 1) == CHAR_SLASH; pos = findEndOfTag(markup, ltPos + 1); boolean isVoidTag = markup.charAt(pos - 2) == CHAR_SLASH; String fullTagExpression = markup.substring( ltPos + (isClosingTag ? 2 : 1), isVoidTag ? pos - 2 : pos - 1 ); String tagName = getTagName(fullTagExpression); if (tagName == null || !isSupportedTag(tagName)) { continue; } if (isClosingTag) { StartTag startTag; do { if (startTagStack.isEmpty()) { break; } startTag = startTagStack.pop(); applySpansForTag(id, startTag, spannedText, styles, scratchStyleMatches); } while (!startTag.name.equals(tagName)); } else if (!isVoidTag) { startTagStack.push(StartTag.buildStartTag(fullTagExpression, spannedText.length())); } break; case CHAR_AMPERSAND: int semiColonEndIndex = markup.indexOf(CHAR_SEMI_COLON, pos + 1); int spaceEndIndex = markup.indexOf(CHAR_SPACE, pos + 1); int entityEndIndex = semiColonEndIndex == -1 ? spaceEndIndex : (spaceEndIndex == -1 ? semiColonEndIndex : Math.min(semiColonEndIndex, spaceEndIndex)); if (entityEndIndex != -1) { applyEntity(markup.substring(pos + 1, entityEndIndex), spannedText); if (entityEndIndex == spaceEndIndex) { spannedText.append(" "); } pos = entityEndIndex + 1; } else { spannedText.append(curr); pos++; } break; default: spannedText.append(curr); pos++; break; } } // apply unclosed tags while (!startTagStack.isEmpty()) { applySpansForTag(id, startTagStack.pop(), spannedText, styles, scratchStyleMatches); } applySpansForTag(id, StartTag.buildWholeCueVirtualTag(), spannedText, styles, scratchStyleMatches ); builder.setText(spannedText); } private static boolean parseCue(String id, Matcher cueHeaderMatcher, ParsableByteArray webvttData, WebvttCue.Builder builder, StringBuilder textBuilder, List styles) { try { // Parse the cue start and end times. builder.setStartTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1))) .setEndTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(2))); } catch (NumberFormatException e) { Log.w(TAG, "Skipping cue with bad header: " + cueHeaderMatcher.group()); return false; } parseCueSettingsList(cueHeaderMatcher.group(3), builder); // Parse the cue text. textBuilder.setLength(0); String line; while (!TextUtils.isEmpty(line = webvttData.readLine())) { if (textBuilder.length() > 0) { textBuilder.append("\n"); } textBuilder.append(line.trim()); } parseCueText(id, textBuilder.toString(), builder, styles); return true; } // Internal methods private static void parseLineAttribute(String s, WebvttCue.Builder builder) throws NumberFormatException { int commaIndex = s.indexOf(','); if (commaIndex != -1) { builder.setLineAnchor(parsePositionAnchor(s.substring(commaIndex + 1))); s = s.substring(0, commaIndex); } else { builder.setLineAnchor(Cue.TYPE_UNSET); } if (s.endsWith("%")) { builder.setLine(WebvttParserUtil.parsePercentage(s)).setLineType(Cue.LINE_TYPE_FRACTION); } else { int lineNumber = Integer.parseInt(s); if (lineNumber < 0) { // WebVTT defines line -1 as last visible row when lineAnchor is ANCHOR_TYPE_START, where-as // Cue defines it to be the first row that's not visible. lineNumber--; } builder.setLine(lineNumber).setLineType(Cue.LINE_TYPE_NUMBER); } } private static void parsePositionAttribute(String s, WebvttCue.Builder builder) throws NumberFormatException { int commaIndex = s.indexOf(','); if (commaIndex != -1) { builder.setPositionAnchor(parsePositionAnchor(s.substring(commaIndex + 1))); s = s.substring(0, commaIndex); } else { builder.setPositionAnchor(Cue.TYPE_UNSET); } builder.setPosition(WebvttParserUtil.parsePercentage(s)); } private static int parsePositionAnchor(String s) { switch (s) { case "start": return Cue.ANCHOR_TYPE_START; case "center": case "middle": return Cue.ANCHOR_TYPE_MIDDLE; case "end": return Cue.ANCHOR_TYPE_END; default: Log.w(TAG, "Invalid anchor value: " + s); return Cue.TYPE_UNSET; } } private static Alignment parseTextAlignment(String s) { switch (s) { case "start": case "left": return Alignment.ALIGN_NORMAL; case "center": case "middle": return Alignment.ALIGN_CENTER; case "end": case "right": return Alignment.ALIGN_OPPOSITE; default: Log.w(TAG, "Invalid alignment value: " + s); return null; } } /** * Find end of tag (>). The position returned is the position of the > plus one (exclusive). * * @param markup The WebVTT cue markup to be parsed. * @param startPos The position from where to start searching for the end of tag. * @return The position of the end of tag plus 1 (one). */ private static int findEndOfTag(String markup, int startPos) { int index = markup.indexOf(CHAR_GREATER_THAN, startPos); return index == -1 ? markup.length() : index + 1; } private static void applyEntity(String entity, SpannableStringBuilder spannedText) { switch (entity) { case ENTITY_LESS_THAN: spannedText.append('<'); break; case ENTITY_GREATER_THAN: spannedText.append('>'); break; case ENTITY_NON_BREAK_SPACE: spannedText.append(' '); break; case ENTITY_AMPERSAND: spannedText.append('&'); break; default: Log.w(TAG, "ignoring unsupported entity: '&" + entity + ";'"); break; } } private static boolean isSupportedTag(String tagName) { switch (tagName) { case TAG_BOLD: case TAG_CLASS: case TAG_ITALIC: case TAG_LANG: case TAG_UNDERLINE: case TAG_VOICE: return true; default: return false; } } private static void applySpansForTag(String cueId, StartTag startTag, SpannableStringBuilder text, List styles, List scratchStyleMatches) { int start = startTag.position; int end = text.length(); switch (startTag.name) { case TAG_BOLD: text.setSpan(new StyleSpan(STYLE_BOLD), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE ); break; case TAG_ITALIC: text.setSpan(new StyleSpan(STYLE_ITALIC), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE ); break; case TAG_UNDERLINE: text.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case TAG_CLASS: applySupportedClasses(text, startTag.classes, start, end); break; case TAG_LANG: case TAG_VOICE: case "": // Case of the "whole cue" virtual tag. break; default: return; } scratchStyleMatches.clear(); getApplicableStyles(styles, cueId, startTag, scratchStyleMatches); int styleMatchesCount = scratchStyleMatches.size(); for (int i = 0; i < styleMatchesCount; i++) { applyStyleToText(text, scratchStyleMatches.get(i).style, start, end); } } private static void applySupportedClasses(SpannableStringBuilder text, String[] classes, int start, int end) { for (String className : classes) { if (ColorParser.isNamedColor(className)) { int color = ColorParser.parseCssColor(className); text.setSpan(new ForegroundColorSpan(color), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } } private static void applyStyleToText(SpannableStringBuilder spannedText, WebvttCssStyle style, int start, int end) { if (style == null) { return; } if (style.getStyle() != WebvttCssStyle.UNSPECIFIED) { spannedText.setSpan(new StyleSpan(style.getStyle()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE ); } if (style.isLinethrough()) { spannedText.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.isUnderline()) { spannedText.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.hasFontColor()) { spannedText.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ); } if (style.hasBackgroundColor()) { spannedText.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ); } if (style.getFontFamily() != null) { spannedText.setSpan(new TypefaceSpan(style.getFontFamily()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE ); } if (style.getTextAlign() != null) { spannedText.setSpan(new AlignmentSpan.Standard(style.getTextAlign()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE ); } switch (style.getFontSizeUnit()) { case WebvttCssStyle.FONT_SIZE_UNIT_PIXEL: spannedText.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE ); break; case WebvttCssStyle.FONT_SIZE_UNIT_EM: spannedText.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE ); break; case WebvttCssStyle.FONT_SIZE_UNIT_PERCENT: spannedText.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE ); break; case WebvttCssStyle.UNSPECIFIED: // Do nothing. break; } } /** * Returns the tag name for the given tag contents. * * @param tagExpression Characters between &lt: and &gt; of a start or end tag. * @return The name of tag. */ private static String getTagName(String tagExpression) { tagExpression = tagExpression.trim(); if (tagExpression.isEmpty()) { return null; } return tagExpression.split("[ \\.]")[0]; } private static void getApplicableStyles(List declaredStyles, String id, StartTag tag, List output) { int styleCount = declaredStyles.size(); for (int i = 0; i < styleCount; i++) { WebvttCssStyle style = declaredStyles.get(i); int score = style.getSpecificityScore(id, tag.name, tag.classes, tag.voice); if (score > 0) { output.add(new StyleMatch(score, style)); } } Collections.sort(output); } private static final class StyleMatch implements Comparable { public final int score; public final WebvttCssStyle style; public StyleMatch(int score, WebvttCssStyle style) { this.score = score; this.style = style; } @Override public int compareTo(@NonNull StyleMatch another) { return this.score - another.score; } } private static final class StartTag { private static final String[] NO_CLASSES = new String[0]; public final String name; public final int position; public final String voice; public final String[] classes; private StartTag(String name, int position, String voice, String[] classes) { this.position = position; this.name = name; this.voice = voice; this.classes = classes; } public static StartTag buildStartTag(String fullTagExpression, int position) { fullTagExpression = fullTagExpression.trim(); if (fullTagExpression.isEmpty()) { return null; } int voiceStartIndex = fullTagExpression.indexOf(" "); String voice; if (voiceStartIndex == -1) { voice = ""; } else { voice = fullTagExpression.substring(voiceStartIndex).trim(); fullTagExpression = fullTagExpression.substring(0, voiceStartIndex); } String[] nameAndClasses = fullTagExpression.split("\\."); String name = nameAndClasses[0]; String[] classes; if (nameAndClasses.length > 1) { classes = Arrays.copyOfRange(nameAndClasses, 1, nameAndClasses.length); } else { classes = NO_CLASSES; } return new StartTag(name, position, voice, classes); } public static StartTag buildWholeCueVirtualTag() { return new StartTag("", 0, "", new String[0]); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/external/exoplayer/text/webvtt/WebvttDecoder.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.novoda.noplayer.external.exoplayer.text.webvtt; import android.text.TextUtils; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; import com.google.android.exoplayer2.text.SubtitleDecoderException; import com.google.android.exoplayer2.text.webvtt.WebvttCssStyle; import com.google.android.exoplayer2.text.webvtt.WebvttCue; import com.google.android.exoplayer2.text.webvtt.WebvttParserUtil; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; import java.util.List; /** * A {@link SimpleSubtitleDecoder} for WebVTT. *

* @see WebVTT specification */ public final class WebvttDecoder extends SimpleSubtitleDecoder { private static final int EVENT_NONE = -1; private static final int EVENT_END_OF_FILE = 0; private static final int EVENT_COMMENT = 1; private static final int EVENT_STYLE_BLOCK = 2; private static final int EVENT_CUE = 3; private static final String COMMENT_START = "NOTE"; private static final String STYLE_START = "STYLE"; private final WebvttCueParser cueParser; private final ParsableByteArray parsableWebvttData; private final WebvttCue.Builder webvttCueBuilder; private final CssParser cssParser; private final List definedStyles; public WebvttDecoder() { super("WebvttDecoder"); cueParser = new WebvttCueParser(); parsableWebvttData = new ParsableByteArray(); webvttCueBuilder = new WebvttCue.Builder(); cssParser = new CssParser(); definedStyles = new ArrayList<>(); } @Override protected WebvttSubtitle decode(byte[] bytes, int length, boolean reset) throws SubtitleDecoderException { parsableWebvttData.reset(bytes, length); // Initialization for consistent starting state. webvttCueBuilder.reset(); definedStyles.clear(); // Validate the first line of the header, and skip the remainder. try { WebvttParserUtil.validateWebvttHeaderLine(parsableWebvttData); } catch (ParserException e) { throw new SubtitleDecoderException(e); } while (!TextUtils.isEmpty(parsableWebvttData.readLine())) { } int event; ArrayList subtitles = new ArrayList<>(); while ((event = getNextEvent(parsableWebvttData)) != EVENT_END_OF_FILE) { if (event == EVENT_COMMENT) { skipComment(parsableWebvttData); } else if (event == EVENT_STYLE_BLOCK) { if (!subtitles.isEmpty()) { throw new SubtitleDecoderException("A style block was found after the first cue."); } parsableWebvttData.readLine(); // Consume the "STYLE" header. WebvttCssStyle styleBlock = cssParser.parseBlock(parsableWebvttData); if (styleBlock != null) { definedStyles.add(styleBlock); } } else if (event == EVENT_CUE) { if (cueParser.parseCue(parsableWebvttData, webvttCueBuilder, definedStyles)) { subtitles.add(webvttCueBuilder.build()); webvttCueBuilder.reset(); } } } return new WebvttSubtitle(subtitles); } /** * Positions the input right before the next event, and returns the kind of event found. Does not * consume any data from such event, if any. * * @return The kind of event found. */ private static int getNextEvent(ParsableByteArray parsableWebvttData) { int foundEvent = EVENT_NONE; int currentInputPosition = 0; while (foundEvent == EVENT_NONE) { currentInputPosition = parsableWebvttData.getPosition(); String line = parsableWebvttData.readLine(); if (line == null) { foundEvent = EVENT_END_OF_FILE; } else if (STYLE_START.equals(line)) { foundEvent = EVENT_STYLE_BLOCK; } else if (COMMENT_START.startsWith(line)) { foundEvent = EVENT_COMMENT; } else { foundEvent = EVENT_CUE; } } parsableWebvttData.setPosition(currentInputPosition); return foundEvent; } private static void skipComment(ParsableByteArray parsableWebvttData) { while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {} } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/external/exoplayer/text/webvtt/WebvttSubtitle.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.novoda.noplayer.external.exoplayer.text.webvtt; import android.text.SpannableStringBuilder; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.webvtt.WebvttCue; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * A representation of a WebVTT subtitle. */ /* package */ final class WebvttSubtitle implements Subtitle { private final List cues; private final int numCues; private final long[] cueTimesUs; private final long[] sortedCueTimesUs; /** * @param cues A list of the cues in this subtitle. */ public WebvttSubtitle(List cues) { this.cues = cues; numCues = cues.size(); cueTimesUs = new long[2 * numCues]; for (int cueIndex = 0; cueIndex < numCues; cueIndex++) { WebvttCue cue = cues.get(cueIndex); int arrayIndex = cueIndex * 2; cueTimesUs[arrayIndex] = cue.startTime; cueTimesUs[arrayIndex + 1] = cue.endTime; } sortedCueTimesUs = Arrays.copyOf(cueTimesUs, cueTimesUs.length); Arrays.sort(sortedCueTimesUs); } @Override public int getNextEventTimeIndex(long timeUs) { int index = Util.binarySearchCeil(sortedCueTimesUs, timeUs, false, false); return index < sortedCueTimesUs.length ? index : C.INDEX_UNSET; } @Override public int getEventTimeCount() { return sortedCueTimesUs.length; } @Override public long getEventTime(int index) { Assertions.checkArgument(index >= 0); Assertions.checkArgument(index < sortedCueTimesUs.length); return sortedCueTimesUs[index]; } @Override public List getCues(long timeUs) { ArrayList list = null; WebvttCue firstNormalCue = null; SpannableStringBuilder normalCueTextBuilder = null; for (int i = 0; i < numCues; i++) { if ((cueTimesUs[i * 2] <= timeUs) && (timeUs < cueTimesUs[i * 2 + 1])) { if (list == null) { list = new ArrayList<>(); } WebvttCue cue = cues.get(i); if (cue.isNormalCue()) { // we want to merge all of the normal cues into a single cue to ensure they are drawn // correctly (i.e. don't overlap) and to emulate roll-up, but only if there are multiple // normal cues, otherwise we can just append the single normal cue if (firstNormalCue == null) { firstNormalCue = cue; } else if (normalCueTextBuilder == null) { normalCueTextBuilder = new SpannableStringBuilder(); normalCueTextBuilder.append(firstNormalCue.text).append("\n").append(cue.text); } else { normalCueTextBuilder.append("\n").append(cue.text); } } else { list.add(cue); } } } if (normalCueTextBuilder != null) { // there were multiple normal cues, so create a new cue with all of the text list.add(new WebvttCue(normalCueTextBuilder)); } else if (firstNormalCue != null) { // there was only a single normal cue, so just add it to the list list.add(firstNormalCue); } if (list != null) { return list; } else { return Collections.emptyList(); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/external/exoplayer/util/ColorParser.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.novoda.noplayer.external.exoplayer.util; import android.text.TextUtils; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Parser for color expressions found in styling formats, e.g. TTML and CSS. * * @see WebVTT CSS Styling * @see Timed Text Markup Language 2 (TTML2) - 10.3.5 **/ public final class ColorParser { private static final String RGB = "rgb"; private static final String RGBA = "rgba"; private static final Pattern RGB_PATTERN = Pattern.compile( "^rgb\\((\\d{1,3}),(\\d{1,3}),(\\d{1,3})\\)$"); private static final Pattern RGBA_PATTERN_INT_ALPHA = Pattern.compile( "^rgba\\((\\d{1,3}),(\\d{1,3}),(\\d{1,3}),(\\d{1,3})\\)$"); private static final Pattern RGBA_PATTERN_FLOAT_ALPHA = Pattern.compile( "^rgba\\((\\d{1,3}),(\\d{1,3}),(\\d{1,3}),(\\d*\\.?\\d*?)\\)$"); private static final Map COLOR_MAP; public static boolean isNamedColor(String expression) { return COLOR_MAP.containsKey(expression); } /** * Parses a TTML color expression. * * @param colorExpression The color expression. * @return The parsed ARGB color. */ public static int parseTtmlColor(String colorExpression) { return parseColorInternal(colorExpression, false); } /** * Parses a CSS color expression. * * @param colorExpression The color expression. * @return The parsed ARGB color. */ public static int parseCssColor(String colorExpression) { return parseColorInternal(colorExpression, true); } private static int parseColorInternal(String colorExpression, boolean alphaHasFloatFormat) { Assertions.checkArgument(!TextUtils.isEmpty(colorExpression)); colorExpression = colorExpression.replace(" ", ""); if (colorExpression.charAt(0) == '#') { // Parse using Long to avoid failure when colorExpression is greater than #7FFFFFFF. int color = (int) Long.parseLong(colorExpression.substring(1), 16); if (colorExpression.length() == 7) { // Set the alpha value color |= 0xFF000000; } else if (colorExpression.length() == 9) { // We have #RRGGBBAA, but we need #AARRGGBB color = ((color & 0xFF) << 24) | (color >>> 8); } else { throw new IllegalArgumentException(); } return color; } else if (colorExpression.startsWith(RGBA)) { Matcher matcher = (alphaHasFloatFormat ? RGBA_PATTERN_FLOAT_ALPHA : RGBA_PATTERN_INT_ALPHA) .matcher(colorExpression); if (matcher.matches()) { return argb( alphaHasFloatFormat ? (int) (255 * Float.parseFloat(matcher.group(4))) : Integer.parseInt(matcher.group(4), 10), Integer.parseInt(matcher.group(1), 10), Integer.parseInt(matcher.group(2), 10), Integer.parseInt(matcher.group(3), 10) ); } } else if (colorExpression.startsWith(RGB)) { Matcher matcher = RGB_PATTERN.matcher(colorExpression); if (matcher.matches()) { return rgb( Integer.parseInt(matcher.group(1), 10), Integer.parseInt(matcher.group(2), 10), Integer.parseInt(matcher.group(3), 10) ); } } else { // we use our own color map Integer color = COLOR_MAP.get(Util.toLowerInvariant(colorExpression)); if (color != null) { return color; } } throw new IllegalArgumentException(); } private static int argb(int alpha, int red, int green, int blue) { return (alpha << 24) | (red << 16) | (green << 8) | blue; } private static int rgb(int red, int green, int blue) { return argb(0xFF, red, green, blue); } static { COLOR_MAP = new HashMap<>(); COLOR_MAP.put("aliceblue", 0xFFF0F8FF); COLOR_MAP.put("antiquewhite", 0xFFFAEBD7); COLOR_MAP.put("aqua", 0xFF00FFFF); COLOR_MAP.put("aquamarine", 0xFF7FFFD4); COLOR_MAP.put("azure", 0xFFF0FFFF); COLOR_MAP.put("beige", 0xFFF5F5DC); COLOR_MAP.put("bisque", 0xFFFFE4C4); COLOR_MAP.put("black", 0xFF000000); COLOR_MAP.put("blanchedalmond", 0xFFFFEBCD); COLOR_MAP.put("blue", 0xFF0000FF); COLOR_MAP.put("blueviolet", 0xFF8A2BE2); COLOR_MAP.put("brown", 0xFFA52A2A); COLOR_MAP.put("burlywood", 0xFFDEB887); COLOR_MAP.put("cadetblue", 0xFF5F9EA0); COLOR_MAP.put("chartreuse", 0xFF7FFF00); COLOR_MAP.put("chocolate", 0xFFD2691E); COLOR_MAP.put("coral", 0xFFFF7F50); COLOR_MAP.put("cornflowerblue", 0xFF6495ED); COLOR_MAP.put("cornsilk", 0xFFFFF8DC); COLOR_MAP.put("crimson", 0xFFDC143C); COLOR_MAP.put("cyan", 0xFF00FFFF); COLOR_MAP.put("darkblue", 0xFF00008B); COLOR_MAP.put("darkcyan", 0xFF008B8B); COLOR_MAP.put("darkgoldenrod", 0xFFB8860B); COLOR_MAP.put("darkgray", 0xFFA9A9A9); COLOR_MAP.put("darkgreen", 0xFF006400); COLOR_MAP.put("darkgrey", 0xFFA9A9A9); COLOR_MAP.put("darkkhaki", 0xFFBDB76B); COLOR_MAP.put("darkmagenta", 0xFF8B008B); COLOR_MAP.put("darkolivegreen", 0xFF556B2F); COLOR_MAP.put("darkorange", 0xFFFF8C00); COLOR_MAP.put("darkorchid", 0xFF9932CC); COLOR_MAP.put("darkred", 0xFF8B0000); COLOR_MAP.put("darksalmon", 0xFFE9967A); COLOR_MAP.put("darkseagreen", 0xFF8FBC8F); COLOR_MAP.put("darkslateblue", 0xFF483D8B); COLOR_MAP.put("darkslategray", 0xFF2F4F4F); COLOR_MAP.put("darkslategrey", 0xFF2F4F4F); COLOR_MAP.put("darkturquoise", 0xFF00CED1); COLOR_MAP.put("darkviolet", 0xFF9400D3); COLOR_MAP.put("deeppink", 0xFFFF1493); COLOR_MAP.put("deepskyblue", 0xFF00BFFF); COLOR_MAP.put("dimgray", 0xFF696969); COLOR_MAP.put("dimgrey", 0xFF696969); COLOR_MAP.put("dodgerblue", 0xFF1E90FF); COLOR_MAP.put("firebrick", 0xFFB22222); COLOR_MAP.put("floralwhite", 0xFFFFFAF0); COLOR_MAP.put("forestgreen", 0xFF228B22); COLOR_MAP.put("fuchsia", 0xFFFF00FF); COLOR_MAP.put("gainsboro", 0xFFDCDCDC); COLOR_MAP.put("ghostwhite", 0xFFF8F8FF); COLOR_MAP.put("gold", 0xFFFFD700); COLOR_MAP.put("goldenrod", 0xFFDAA520); COLOR_MAP.put("gray", 0xFF808080); COLOR_MAP.put("green", 0xFF008000); COLOR_MAP.put("greenyellow", 0xFFADFF2F); COLOR_MAP.put("grey", 0xFF808080); COLOR_MAP.put("honeydew", 0xFFF0FFF0); COLOR_MAP.put("hotpink", 0xFFFF69B4); COLOR_MAP.put("indianred", 0xFFCD5C5C); COLOR_MAP.put("indigo", 0xFF4B0082); COLOR_MAP.put("ivory", 0xFFFFFFF0); COLOR_MAP.put("khaki", 0xFFF0E68C); COLOR_MAP.put("lavender", 0xFFE6E6FA); COLOR_MAP.put("lavenderblush", 0xFFFFF0F5); COLOR_MAP.put("lawngreen", 0xFF7CFC00); COLOR_MAP.put("lemonchiffon", 0xFFFFFACD); COLOR_MAP.put("lightblue", 0xFFADD8E6); COLOR_MAP.put("lightcoral", 0xFFF08080); COLOR_MAP.put("lightcyan", 0xFFE0FFFF); COLOR_MAP.put("lightgoldenrodyellow", 0xFFFAFAD2); COLOR_MAP.put("lightgray", 0xFFD3D3D3); COLOR_MAP.put("lightgreen", 0xFF90EE90); COLOR_MAP.put("lightgrey", 0xFFD3D3D3); COLOR_MAP.put("lightpink", 0xFFFFB6C1); COLOR_MAP.put("lightsalmon", 0xFFFFA07A); COLOR_MAP.put("lightseagreen", 0xFF20B2AA); COLOR_MAP.put("lightskyblue", 0xFF87CEFA); COLOR_MAP.put("lightslategray", 0xFF778899); COLOR_MAP.put("lightslategrey", 0xFF778899); COLOR_MAP.put("lightsteelblue", 0xFFB0C4DE); COLOR_MAP.put("lightyellow", 0xFFFFFFE0); COLOR_MAP.put("lime", 0xFF00FF00); COLOR_MAP.put("limegreen", 0xFF32CD32); COLOR_MAP.put("linen", 0xFFFAF0E6); COLOR_MAP.put("magenta", 0xFFFF00FF); COLOR_MAP.put("maroon", 0xFF800000); COLOR_MAP.put("mediumaquamarine", 0xFF66CDAA); COLOR_MAP.put("mediumblue", 0xFF0000CD); COLOR_MAP.put("mediumorchid", 0xFFBA55D3); COLOR_MAP.put("mediumpurple", 0xFF9370DB); COLOR_MAP.put("mediumseagreen", 0xFF3CB371); COLOR_MAP.put("mediumslateblue", 0xFF7B68EE); COLOR_MAP.put("mediumspringgreen", 0xFF00FA9A); COLOR_MAP.put("mediumturquoise", 0xFF48D1CC); COLOR_MAP.put("mediumvioletred", 0xFFC71585); COLOR_MAP.put("midnightblue", 0xFF191970); COLOR_MAP.put("mintcream", 0xFFF5FFFA); COLOR_MAP.put("mistyrose", 0xFFFFE4E1); COLOR_MAP.put("moccasin", 0xFFFFE4B5); COLOR_MAP.put("navajowhite", 0xFFFFDEAD); COLOR_MAP.put("navy", 0xFF000080); COLOR_MAP.put("oldlace", 0xFFFDF5E6); COLOR_MAP.put("olive", 0xFF808000); COLOR_MAP.put("olivedrab", 0xFF6B8E23); COLOR_MAP.put("orange", 0xFFFFA500); COLOR_MAP.put("orangered", 0xFFFF4500); COLOR_MAP.put("orchid", 0xFFDA70D6); COLOR_MAP.put("palegoldenrod", 0xFFEEE8AA); COLOR_MAP.put("palegreen", 0xFF98FB98); COLOR_MAP.put("paleturquoise", 0xFFAFEEEE); COLOR_MAP.put("palevioletred", 0xFFDB7093); COLOR_MAP.put("papayawhip", 0xFFFFEFD5); COLOR_MAP.put("peachpuff", 0xFFFFDAB9); COLOR_MAP.put("peru", 0xFFCD853F); COLOR_MAP.put("pink", 0xFFFFC0CB); COLOR_MAP.put("plum", 0xFFDDA0DD); COLOR_MAP.put("powderblue", 0xFFB0E0E6); COLOR_MAP.put("purple", 0xFF800080); COLOR_MAP.put("rebeccapurple", 0xFF663399); COLOR_MAP.put("red", 0xFFFF0000); COLOR_MAP.put("rosybrown", 0xFFBC8F8F); COLOR_MAP.put("royalblue", 0xFF4169E1); COLOR_MAP.put("saddlebrown", 0xFF8B4513); COLOR_MAP.put("salmon", 0xFFFA8072); COLOR_MAP.put("sandybrown", 0xFFF4A460); COLOR_MAP.put("seagreen", 0xFF2E8B57); COLOR_MAP.put("seashell", 0xFFFFF5EE); COLOR_MAP.put("sienna", 0xFFA0522D); COLOR_MAP.put("silver", 0xFFC0C0C0); COLOR_MAP.put("skyblue", 0xFF87CEEB); COLOR_MAP.put("slateblue", 0xFF6A5ACD); COLOR_MAP.put("slategray", 0xFF708090); COLOR_MAP.put("slategrey", 0xFF708090); COLOR_MAP.put("snow", 0xFFFFFAFA); COLOR_MAP.put("springgreen", 0xFF00FF7F); COLOR_MAP.put("steelblue", 0xFF4682B4); COLOR_MAP.put("tan", 0xFFD2B48C); COLOR_MAP.put("teal", 0xFF008080); COLOR_MAP.put("thistle", 0xFFD8BFD8); COLOR_MAP.put("tomato", 0xFFFF6347); COLOR_MAP.put("transparent", 0x00000000); COLOR_MAP.put("turquoise", 0xFF40E0D0); COLOR_MAP.put("violet", 0xFFEE82EE); COLOR_MAP.put("wheat", 0xFFF5DEB3); COLOR_MAP.put("white", 0xFFFFFFFF); COLOR_MAP.put("whitesmoke", 0xFFF5F5F5); COLOR_MAP.put("yellow", 0xFFFFFF00); COLOR_MAP.put("yellowgreen", 0xFF9ACD32); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/Clock.java ================================================ package com.novoda.noplayer.internal; import java.io.Serializable; public interface Clock extends Serializable { long getCurrentTime(); } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/Heart.java ================================================ package com.novoda.noplayer.internal; import android.os.Handler; import com.novoda.noplayer.NoPlayer; @SuppressWarnings("checkstyle:FinalClass") // We cannot make it final as we need to mock it in tests public class Heart { private static final long HEART_BEAT_FREQUENCY_IN_MILLIS = 500; private final Handler handler; private final long heartbeatFrequency; private Heartbeat heartbeatAction; private boolean beating; public static Heart newInstance(Handler handler) { return new Heart(handler, HEART_BEAT_FREQUENCY_IN_MILLIS); } private Heart(Handler handler, long heartbeatFrequencyInMillis) { this.handler = handler; this.heartbeatFrequency = heartbeatFrequencyInMillis; } public void bind(Heartbeat onHeartbeat) { this.heartbeatAction = onHeartbeat; } public void startBeatingHeart() { if (heartbeatAction == null) { throw new IllegalStateException("You must call bind() with a valid non-null " + Heartbeat.class.getSimpleName()); } stopBeatingHeart(); beating = true; handler.post(heartbeat); } private final Runnable heartbeat = new Runnable() { @Override public void run() { handler.post(heartbeatAction); scheduleNextBeat(); } }; private void scheduleNextBeat() { handler.postDelayed(heartbeat, heartbeatFrequency); } public void stopBeatingHeart() { beating = false; handler.removeCallbacks(heartbeat); } public void forceBeat() { if (heartbeatAction == null) { throw new IllegalStateException("You must call bind() with a valid non-null " + Heartbeat.class.getSimpleName()); } handler.post(heartbeatAction); } public boolean isBeating() { return beating; } public static class Heartbeat implements Runnable { private final NoPlayer.HeartbeatCallback callback; private final NoPlayer player; public Heartbeat(NoPlayer.HeartbeatCallback callback, NoPlayer player) { this.callback = callback; this.player = player; } @Override public void run() { if (player.isPlaying()) { callback.onBeat(player); } } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/SystemClock.java ================================================ package com.novoda.noplayer.internal; public class SystemClock implements Clock { @Override public long getCurrentTime() { return System.currentTimeMillis(); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/drm/provision/HttpPostingProvisionExecutor.java ================================================ package com.novoda.noplayer.internal.drm.provision; import com.novoda.noplayer.drm.ModularDrmProvisionRequest; import java.io.IOException; import java.nio.charset.Charset; class HttpPostingProvisionExecutor implements ProvisionExecutor { private static final String PARAMETER_SIGNED_REQUEST = "&signedRequest="; private final HttpUrlConnectionPoster httpPoster; private final ProvisioningCapabilities capabilities; HttpPostingProvisionExecutor(HttpUrlConnectionPoster httpPoster, ProvisioningCapabilities capabilities) { this.httpPoster = httpPoster; this.capabilities = capabilities; } @Override public byte[] execute(ModularDrmProvisionRequest request) throws IOException, UnableToProvisionException { if (isIncapableOfProvisioning()) { throw new UnableToProvisionException(); } String provisioningUrl = buildProvisioningUrl(request); return httpPoster.post(provisioningUrl); } private boolean isIncapableOfProvisioning() { return !capabilities.canProvision(); } private String buildProvisioningUrl(ModularDrmProvisionRequest request) { return request.url() + PARAMETER_SIGNED_REQUEST + new String(request.data(), Charset.forName("UTF-8")); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/drm/provision/HttpUrlConnectionPoster.java ================================================ package com.novoda.noplayer.internal.drm.provision; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; class HttpUrlConnectionPoster { private static final String POST_REQUEST_METHOD = "POST"; private static final int RESPONSE_BUFFER_SIZE = 1024 * 4; byte[] post(String url) throws IOException { HttpURLConnection urlConnection = null; try { urlConnection = (HttpURLConnection) new URL(url).openConnection(); urlConnection.setRequestMethod(POST_REQUEST_METHOD); urlConnection.setDoInput(true); return byteArrayFrom(urlConnection); } finally { if (urlConnection != null) { urlConnection.disconnect(); } } } private byte[] byteArrayFrom(HttpURLConnection urlConnection) throws IOException { InputStream inputStream = urlConnection.getInputStream(); try { return byteArrayFrom(inputStream); } finally { inputStream.close(); } } private byte[] byteArrayFrom(InputStream inputStream) throws IOException { byte[] buffer = new byte[RESPONSE_BUFFER_SIZE]; ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); } try { return outputStream.toByteArray(); } finally { outputStream.close(); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/drm/provision/ProvisionExecutor.java ================================================ package com.novoda.noplayer.internal.drm.provision; import com.novoda.noplayer.drm.ModularDrmProvisionRequest; import java.io.IOException; public interface ProvisionExecutor { byte[] execute(ModularDrmProvisionRequest request) throws IOException, UnableToProvisionException; } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/drm/provision/ProvisionExecutorCreator.java ================================================ package com.novoda.noplayer.internal.drm.provision; public class ProvisionExecutorCreator { public ProvisionExecutor create() { HttpUrlConnectionPoster httpPoster = new HttpUrlConnectionPoster(); ProvisioningCapabilities capabilities = ProvisioningCapabilities.newInstance(); return new HttpPostingProvisionExecutor(httpPoster, capabilities); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/drm/provision/ProvisioningCapabilities.java ================================================ package com.novoda.noplayer.internal.drm.provision; import android.os.Build; import android.support.annotation.VisibleForTesting; class ProvisioningCapabilities { private final int deviceOsVersion; static ProvisioningCapabilities newInstance() { return new ProvisioningCapabilities(Build.VERSION.SDK_INT); } @VisibleForTesting ProvisioningCapabilities(int deviceOsVersion) { this.deviceOsVersion = deviceOsVersion; } boolean canProvision() { return deviceOsVersion >= Build.VERSION_CODES.JELLY_BEAN_MR2; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/drm/provision/UnableToProvisionException.java ================================================ package com.novoda.noplayer.internal.drm.provision; import android.os.Build; public class UnableToProvisionException extends Exception { UnableToProvisionException() { super("Device is : " + Build.VERSION.SDK_INT + ", which is does not support provisioning, " + Build.VERSION_CODES.JELLY_BEAN_MR2 + " and higher is required"); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/BandwidthMeterCreator.java ================================================ package com.novoda.noplayer.internal.exoplayer; import android.content.Context; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; class BandwidthMeterCreator { private final Context context; BandwidthMeterCreator(Context context) { this.context = context; } DefaultBandwidthMeter create(long maxInitialBitrate) { return new DefaultBandwidthMeter.Builder(context) .setInitialBitrateEstimate(maxInitialBitrate) .build(); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/CompositeTrackSelector.java ================================================ package com.novoda.noplayer.internal.exoplayer; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.novoda.noplayer.ContentType; import com.novoda.noplayer.internal.exoplayer.mediasource.ExoPlayerAudioTrackSelector; import com.novoda.noplayer.internal.exoplayer.mediasource.ExoPlayerSubtitleTrackSelector; import com.novoda.noplayer.internal.exoplayer.mediasource.ExoPlayerVideoTrackSelector; import com.novoda.noplayer.internal.utils.Optional; import com.novoda.noplayer.model.AudioTracks; import com.novoda.noplayer.model.PlayerAudioTrack; import com.novoda.noplayer.model.PlayerSubtitleTrack; import com.novoda.noplayer.model.PlayerVideoTrack; import java.util.List; class CompositeTrackSelector { private final DefaultTrackSelector defaultTrackSelector; private final ExoPlayerAudioTrackSelector audioTrackSelector; private final ExoPlayerVideoTrackSelector videoTrackSelector; private final ExoPlayerSubtitleTrackSelector subtitleTrackSelector; CompositeTrackSelector(DefaultTrackSelector defaultTrackSelector, ExoPlayerAudioTrackSelector audioTrackSelector, ExoPlayerVideoTrackSelector videoTrackSelector, ExoPlayerSubtitleTrackSelector subtitleTrackSelector) { this.defaultTrackSelector = defaultTrackSelector; this.audioTrackSelector = audioTrackSelector; this.videoTrackSelector = videoTrackSelector; this.subtitleTrackSelector = subtitleTrackSelector; } TrackSelector trackSelector() { return defaultTrackSelector; } boolean selectAudioTrack(PlayerAudioTrack audioTrack, RendererTypeRequester rendererTypeRequester) { return audioTrackSelector.selectAudioTrack(audioTrack, rendererTypeRequester); } AudioTracks getAudioTracks(RendererTypeRequester rendererTypeRequester) { return audioTrackSelector.getAudioTracks(rendererTypeRequester); } boolean clearAudioTrack(RendererTypeRequester rendererTypeRequester) { return audioTrackSelector.clearAudioTrack(rendererTypeRequester); } boolean selectVideoTrack(PlayerVideoTrack videoTrack, RendererTypeRequester rendererTypeRequester) { return videoTrackSelector.selectVideoTrack(videoTrack, rendererTypeRequester); } List getVideoTracks(RendererTypeRequester rendererTypeRequester, ContentType contentType) { return videoTrackSelector.getVideoTracks(rendererTypeRequester, contentType); } Optional getSelectedVideoTrack(SimpleExoPlayer exoPlayer, RendererTypeRequester rendererTypeRequester, ContentType contentType) { return videoTrackSelector.getSelectedVideoTrack(exoPlayer, rendererTypeRequester, contentType); } boolean clearVideoTrack(RendererTypeRequester rendererTypeRequester) { return videoTrackSelector.clearVideoTrack(rendererTypeRequester); } boolean selectTextTrack(PlayerSubtitleTrack subtitleTrack, RendererTypeRequester rendererTypeRequester) { return subtitleTrackSelector.selectTextTrack(subtitleTrack, rendererTypeRequester); } List getSubtitleTracks(RendererTypeRequester rendererTypeRequester) { return subtitleTrackSelector.getSubtitleTracks(rendererTypeRequester); } boolean clearSubtitleTrack(RendererTypeRequester rendererTypeRequester) { return subtitleTrackSelector.clearSubtitleTrack(rendererTypeRequester); } void clearMaxVideoBitrate() { videoTrackSelector.clearMaxVideoBitrate(); } void setMaxVideoBitrate(int maxVideoBitrate) { videoTrackSelector.setMaxVideoBitrate(maxVideoBitrate); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/CompositeTrackSelectorCreator.java ================================================ package com.novoda.noplayer.internal.exoplayer; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.util.Clock; import com.novoda.noplayer.Options; import com.novoda.noplayer.internal.exoplayer.mediasource.ExoPlayerAudioTrackSelector; import com.novoda.noplayer.internal.exoplayer.mediasource.ExoPlayerSubtitleTrackSelector; import com.novoda.noplayer.internal.exoplayer.mediasource.ExoPlayerTrackSelector; import com.novoda.noplayer.internal.exoplayer.mediasource.ExoPlayerVideoTrackSelector; class CompositeTrackSelectorCreator { CompositeTrackSelector create(Options options, DefaultBandwidthMeter bandwidthMeter) { TrackSelection.Factory adaptiveTrackSelectionFactory = new AdaptiveTrackSelection.Factory( bandwidthMeter, options.minDurationBeforeQualityIncreaseInMillis(), AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, AdaptiveTrackSelection.DEFAULT_BANDWIDTH_FRACTION, AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, AdaptiveTrackSelection.DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, Clock.DEFAULT ); DefaultTrackSelector trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory); DefaultTrackSelector.Parameters trackSelectorParameters = trackSelector.buildUponParameters() .setMaxVideoBitrate(options.maxVideoBitrate()) .build(); trackSelector.setParameters(trackSelectorParameters); ExoPlayerTrackSelector exoPlayerTrackSelector = ExoPlayerTrackSelector.newInstance(trackSelector); ExoPlayerAudioTrackSelector audioTrackSelector = new ExoPlayerAudioTrackSelector(exoPlayerTrackSelector); ExoPlayerVideoTrackSelector videoTrackSelector = new ExoPlayerVideoTrackSelector(exoPlayerTrackSelector); ExoPlayerSubtitleTrackSelector subtitleTrackSelector = new ExoPlayerSubtitleTrackSelector(exoPlayerTrackSelector); return new CompositeTrackSelector(trackSelector, audioTrackSelector, videoTrackSelector, subtitleTrackSelector); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerCreator.java ================================================ package com.novoda.noplayer.internal.exoplayer; import android.content.Context; import android.support.annotation.NonNull; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.text.SubtitleDecoderFactory; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreator; import com.novoda.noplayer.text.NoPlayerSubtitleDecoderFactory; import static com.novoda.noplayer.internal.exoplayer.SimpleRenderersFactory.EXTENSION_RENDERER_MODE_OFF; class ExoPlayerCreator { private static final long DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS = 5000; private final Context context; ExoPlayerCreator(Context context) { this.context = context; } @NonNull public SimpleExoPlayer create(DrmSessionCreator drmSessionCreator, DefaultDrmSessionEventListener drmSessionEventListener, MediaCodecSelector mediaCodecSelector, TrackSelector trackSelector) { DrmSessionManager drmSessionManager = drmSessionCreator.create(drmSessionEventListener); SubtitleDecoderFactory subtitleDecoderFactory = new NoPlayerSubtitleDecoderFactory(); RenderersFactory renderersFactory = new SimpleRenderersFactory( context, EXTENSION_RENDERER_MODE_OFF, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS, mediaCodecSelector, subtitleDecoderFactory ); DefaultLoadControl loadControl = new DefaultLoadControl(); return ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector, loadControl, drmSessionManager); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerCueMapper.java ================================================ package com.novoda.noplayer.internal.exoplayer; import com.google.android.exoplayer2.text.Cue; import com.novoda.noplayer.model.NoPlayerCue; import com.novoda.noplayer.model.TextCues; import java.util.ArrayList; import java.util.Collections; import java.util.List; final class ExoPlayerCueMapper { private ExoPlayerCueMapper() { // static class. } static TextCues map(List cues) { if (cues == null) { return TextCues.of(Collections.emptyList()); } List noPlayerCues = new ArrayList<>(cues.size()); for (Cue cue : cues) { NoPlayerCue noPlayerCue = new NoPlayerCue( cue.text, cue.textAlignment, cue.bitmap, cue.line, cue.lineType, cue.lineAnchor, cue.position, cue.positionAnchor, cue.size, cue.bitmapHeight, cue.windowColorSet, cue.windowColor ); noPlayerCues.add(noPlayerCue); } return TextCues.of(noPlayerCues); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerFacade.java ================================================ package com.novoda.noplayer.internal.exoplayer; import android.net.Uri; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.novoda.noplayer.Options; import com.novoda.noplayer.PlayerSurfaceHolder; import com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreator; import com.novoda.noplayer.internal.exoplayer.forwarder.ExoPlayerForwarder; import com.novoda.noplayer.internal.exoplayer.mediasource.MediaSourceFactory; import com.novoda.noplayer.internal.utils.AndroidDeviceVersion; import com.novoda.noplayer.internal.utils.Optional; import com.novoda.noplayer.model.AudioTracks; import com.novoda.noplayer.model.PlayerAudioTrack; import com.novoda.noplayer.model.PlayerSubtitleTrack; import com.novoda.noplayer.model.PlayerVideoTrack; import java.util.List; class ExoPlayerFacade { private static final boolean DO_NOT_RESET_STATE = false; private final BandwidthMeterCreator bandwidthMeterCreator; private final AndroidDeviceVersion androidDeviceVersion; private final MediaSourceFactory mediaSourceFactory; private final CompositeTrackSelectorCreator trackSelectorCreator; private final ExoPlayerCreator exoPlayerCreator; private final RendererTypeRequesterCreator rendererTypeRequesterCreator; @Nullable private SimpleExoPlayer exoPlayer; @Nullable private CompositeTrackSelector compositeTrackSelector; @Nullable private RendererTypeRequester rendererTypeRequester; @Nullable private Options options; ExoPlayerFacade(BandwidthMeterCreator bandwidthMeterCreator, AndroidDeviceVersion androidDeviceVersion, MediaSourceFactory mediaSourceFactory, CompositeTrackSelectorCreator trackSelectorCreator, ExoPlayerCreator exoPlayerCreator, RendererTypeRequesterCreator rendererTypeRequesterCreator) { this.bandwidthMeterCreator = bandwidthMeterCreator; this.androidDeviceVersion = androidDeviceVersion; this.mediaSourceFactory = mediaSourceFactory; this.trackSelectorCreator = trackSelectorCreator; this.exoPlayerCreator = exoPlayerCreator; this.rendererTypeRequesterCreator = rendererTypeRequesterCreator; } boolean isPlaying() { return exoPlayer != null && exoPlayer.getPlayWhenReady(); } long playheadPositionInMillis() throws IllegalStateException { assertVideoLoaded(); return exoPlayer.getCurrentPosition(); } long mediaDurationInMillis() throws IllegalStateException { assertVideoLoaded(); return exoPlayer.getDuration(); } int bufferPercentage() throws IllegalStateException { assertVideoLoaded(); return exoPlayer.getBufferedPercentage(); } void play(long positionInMillis) throws IllegalStateException { seekTo(positionInMillis); play(); } void play() throws IllegalStateException { assertVideoLoaded(); exoPlayer.setPlayWhenReady(true); } void pause() throws IllegalStateException { assertVideoLoaded(); exoPlayer.setPlayWhenReady(false); } void seekTo(long positionInMillis) throws IllegalStateException { assertVideoLoaded(); exoPlayer.seekTo(positionInMillis); } void release() { if (exoPlayer != null) { exoPlayer.release(); exoPlayer = null; } } void loadVideo(PlayerSurfaceHolder playerSurfaceHolder, DrmSessionCreator drmSessionCreator, Uri uri, Options options, ExoPlayerForwarder forwarder, MediaCodecSelector mediaCodecSelector) { this.options = options; DefaultBandwidthMeter bandwidthMeter = bandwidthMeterCreator.create(options.maxInitialBitrate()); compositeTrackSelector = trackSelectorCreator.create(options, bandwidthMeter); exoPlayer = exoPlayerCreator.create( drmSessionCreator, forwarder.drmSessionEventListener(), mediaCodecSelector, compositeTrackSelector.trackSelector() ); rendererTypeRequester = rendererTypeRequesterCreator.createfrom(exoPlayer); exoPlayer.addListener(forwarder.exoPlayerEventListener()); exoPlayer.addAnalyticsListener(forwarder.analyticsListener()); exoPlayer.addVideoListener(forwarder.videoListener()); setMovieAudioAttributes(exoPlayer); MediaSource mediaSource = mediaSourceFactory.create( options, uri, forwarder.mediaSourceEventListener(), bandwidthMeter ); attachToSurface(playerSurfaceHolder); boolean hasInitialPosition = options.getInitialPositionInMillis().isPresent(); if (hasInitialPosition) { Long initialPositionInMillis = options.getInitialPositionInMillis().get(); exoPlayer.seekTo(initialPositionInMillis); } exoPlayer.prepare(mediaSource, !hasInitialPosition, DO_NOT_RESET_STATE); } private void setMovieAudioAttributes(SimpleExoPlayer exoPlayer) { if (androidDeviceVersion.isLollipopTwentyOneOrAbove()) { AudioAttributes audioAttributes = new AudioAttributes.Builder() .setContentType(C.CONTENT_TYPE_MOVIE) .build(); exoPlayer.setAudioAttributes(audioAttributes); } } private void attachToSurface(PlayerSurfaceHolder playerSurfaceHolder) { playerSurfaceHolder.attach(exoPlayer); } AudioTracks getAudioTracks() throws IllegalStateException { assertVideoLoaded(); return compositeTrackSelector.getAudioTracks(rendererTypeRequester); } boolean selectAudioTrack(PlayerAudioTrack audioTrack) throws IllegalStateException { assertVideoLoaded(); return compositeTrackSelector.selectAudioTrack(audioTrack, rendererTypeRequester); } boolean clearAudioTrackSelection() { assertVideoLoaded(); return compositeTrackSelector.clearAudioTrack(rendererTypeRequester); } boolean selectVideoTrack(PlayerVideoTrack playerVideoTrack) { assertVideoLoaded(); return compositeTrackSelector.selectVideoTrack(playerVideoTrack, rendererTypeRequester); } Optional getSelectedVideoTrack() { assertVideoLoaded(); return compositeTrackSelector.getSelectedVideoTrack(exoPlayer, rendererTypeRequester, options.contentType()); } List getVideoTracks() { assertVideoLoaded(); return compositeTrackSelector.getVideoTracks(rendererTypeRequester, options.contentType()); } boolean clearVideoTrackSelection() { assertVideoLoaded(); return compositeTrackSelector.clearVideoTrack(rendererTypeRequester); } void setSubtitleRendererOutput(TextRendererOutput textRendererOutput) throws IllegalStateException { assertVideoLoaded(); exoPlayer.addTextOutput(textRendererOutput.output()); } void removeSubtitleRendererOutput(TextRendererOutput textRendererOutput) throws IllegalStateException { assertVideoLoaded(); exoPlayer.removeTextOutput(textRendererOutput.output()); } boolean selectSubtitleTrack(PlayerSubtitleTrack subtitleTrack) throws IllegalStateException { assertVideoLoaded(); return compositeTrackSelector.selectTextTrack(subtitleTrack, rendererTypeRequester); } List getSubtitleTracks() throws IllegalStateException { assertVideoLoaded(); return compositeTrackSelector.getSubtitleTracks(rendererTypeRequester); } boolean hasPlayedContent() { return exoPlayer != null; } boolean clearSubtitleTrackSelection() throws IllegalStateException { assertVideoLoaded(); return compositeTrackSelector.clearSubtitleTrack(rendererTypeRequester); } void setRepeating(boolean repeating) { assertVideoLoaded(); exoPlayer.setRepeatMode(repeating ? Player.REPEAT_MODE_ALL : Player.REPEAT_MODE_OFF); } void setVolume(float volume) { assertVideoLoaded(); exoPlayer.setVolume(volume); } float getVolume() { assertVideoLoaded(); return exoPlayer.getVolume(); } void clearMaxVideoBitrate() { assertVideoLoaded(); compositeTrackSelector.clearMaxVideoBitrate(); } void setMaxVideoBitrate(int maxVideoBitrate) { assertVideoLoaded(); compositeTrackSelector.setMaxVideoBitrate(maxVideoBitrate); } private void assertVideoLoaded() { if (exoPlayer == null) { throw new IllegalStateException("Video must be loaded before trying to interact with the player"); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerInformation.java ================================================ package com.novoda.noplayer.internal.exoplayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.novoda.noplayer.PlayerInformation; import com.novoda.noplayer.PlayerType; class ExoPlayerInformation implements PlayerInformation { @Override public PlayerType getPlayerType() { return PlayerType.EXO_PLAYER; } @Override public String getVersion() { return ExoPlayerLibraryInfo.VERSION; } @Override public String getName() { return "ExoPlayer"; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerTwoImpl.java ================================================ package com.novoda.noplayer.internal.exoplayer; import android.net.Uri; import android.support.annotation.Nullable; import android.view.View; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.novoda.noplayer.Listeners; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.Options; import com.novoda.noplayer.PlayerInformation; import com.novoda.noplayer.PlayerState; import com.novoda.noplayer.PlayerView; import com.novoda.noplayer.internal.Heart; import com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreator; import com.novoda.noplayer.internal.exoplayer.forwarder.ExoPlayerForwarder; import com.novoda.noplayer.internal.listeners.PlayerListenersHolder; import com.novoda.noplayer.internal.utils.Optional; import com.novoda.noplayer.model.AudioTracks; import com.novoda.noplayer.model.LoadTimeout; import com.novoda.noplayer.model.PlayerAudioTrack; import com.novoda.noplayer.model.PlayerSubtitleTrack; import com.novoda.noplayer.model.PlayerVideoTrack; import com.novoda.noplayer.model.Timeout; import java.util.List; // Not much we can do, wrapping ExoPlayer is a lot of work @SuppressWarnings("PMD.GodClass") class ExoPlayerTwoImpl implements NoPlayer { private final ExoPlayerFacade exoPlayer; private final PlayerListenersHolder listenersHolder; private final ExoPlayerForwarder forwarder; private final Heart heart; private final DrmSessionCreator drmSessionCreator; private final MediaCodecSelector mediaCodecSelector; private final LoadTimeout loadTimeout; @Nullable private PlayerView playerView; private int videoWidth; private int videoHeight; private TextRendererOutput textRendererOutput; ExoPlayerTwoImpl(ExoPlayerFacade exoPlayer, PlayerListenersHolder listenersHolder, ExoPlayerForwarder exoPlayerForwarder, LoadTimeout loadTimeoutParam, Heart heart, DrmSessionCreator drmSessionCreator, MediaCodecSelector mediaCodecSelector) { this.exoPlayer = exoPlayer; this.listenersHolder = listenersHolder; this.loadTimeout = loadTimeoutParam; this.forwarder = exoPlayerForwarder; this.heart = heart; this.drmSessionCreator = drmSessionCreator; this.mediaCodecSelector = mediaCodecSelector; } void initialise() { heart.bind(new Heart.Heartbeat(listenersHolder.getHeartbeatCallbacks(), this)); forwarder.bind(listenersHolder.getPreparedListeners(), this); forwarder.bind(listenersHolder.getCompletionListeners(), listenersHolder.getStateChangedListeners()); forwarder.bind(listenersHolder.getErrorListeners()); forwarder.bind(listenersHolder.getBufferStateListeners()); forwarder.bind(listenersHolder.getVideoSizeChangedListeners()); forwarder.bind(listenersHolder.getBitrateChangedListeners()); forwarder.bind(listenersHolder.getInfoListeners()); forwarder.bind(listenersHolder.getDroppedVideoFramesListeners()); listenersHolder.addPreparedListener(new PreparedListener() { @Override public void onPrepared(PlayerState playerState) { loadTimeout.cancel(); } }); listenersHolder.addErrorListener(new ErrorListener() { @Override public void onError(PlayerError error) { reset(); } }); listenersHolder.addVideoSizeChangedListener(new VideoSizeChangedListener() { @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { videoWidth = width; videoHeight = height; } }); } @Override public boolean isPlaying() { return exoPlayer.isPlaying(); } @Override public int videoWidth() { return videoWidth; } @Override public int videoHeight() { return videoHeight; } @Override public long playheadPositionInMillis() throws IllegalStateException { return exoPlayer.playheadPositionInMillis(); } @Override public long mediaDurationInMillis() throws IllegalStateException { return exoPlayer.mediaDurationInMillis(); } @Override public int bufferPercentage() throws IllegalStateException { return exoPlayer.bufferPercentage(); } @Override public void setRepeating(boolean repeating) { exoPlayer.setRepeating(repeating); } @Override public void setVolume(float volume) { exoPlayer.setVolume(volume); } @Override public float getVolume() { return exoPlayer.getVolume(); } @Override public void clearMaxVideoBitrate() { exoPlayer.clearMaxVideoBitrate(); } @Override public void setMaxVideoBitrate(int maxVideoBitrate) { exoPlayer.setMaxVideoBitrate(maxVideoBitrate); } @Override public Listeners getListeners() { return listenersHolder; } @Override public void play() throws IllegalStateException { heart.startBeatingHeart(); exoPlayer.play(); listenersHolder.getStateChangedListeners().onVideoPlaying(); } @Override public void playAt(long positionInMillis) throws IllegalStateException { seekTo(positionInMillis); play(); } @Override public void pause() throws IllegalStateException { exoPlayer.pause(); listenersHolder.getStateChangedListeners().onVideoPaused(); if (heart.isBeating()) { heart.stopBeatingHeart(); heart.forceBeat(); } } @Override public void seekTo(long positionInMillis) throws IllegalStateException { exoPlayer.seekTo(positionInMillis); } @Override public void stop() { reset(); listenersHolder.getStateChangedListeners().onVideoStopped(); } @Override public void release() { stop(); listenersHolder.clear(); } private void reset() { listenersHolder.resetState(); loadTimeout.cancel(); heart.stopBeatingHeart(); exoPlayer.release(); destroySurfaceByHidingVideoContainer(); } private void destroySurfaceByHidingVideoContainer() { if (playerView != null) { playerView.getContainerView().setVisibility(View.GONE); } } @Override public void loadVideo(final Uri uri, final Options options) { if (exoPlayer.hasPlayedContent()) { stop(); } assertPlayerViewIsAttached(); exoPlayer.loadVideo(playerView.getPlayerSurfaceHolder(), drmSessionCreator, uri, options, forwarder, mediaCodecSelector); createSurfaceByShowingVideoContainer(); } private void assertPlayerViewIsAttached() { if (playerView == null) { throw new IllegalStateException("A PlayerView must be attached in order to loadVideo"); } } private void createSurfaceByShowingVideoContainer() { playerView.getContainerView().setVisibility(View.VISIBLE); } @Override public void loadVideoWithTimeout(Uri uri, Options options, Timeout timeout, LoadTimeoutCallback loadTimeoutCallback) { loadTimeout.start(timeout, loadTimeoutCallback); loadVideo(uri, options); } @Override public PlayerInformation getPlayerInformation() { return new ExoPlayerInformation(); } @Override public void attach(PlayerView playerView) { this.playerView = playerView; listenersHolder.addStateChangedListener(playerView.getStateChangedListener()); listenersHolder.addVideoSizeChangedListener(playerView.getVideoSizeChangedListener()); } @Override public void detach(PlayerView playerView) { listenersHolder.removeStateChangedListener(playerView.getStateChangedListener()); listenersHolder.removeVideoSizeChangedListener(playerView.getVideoSizeChangedListener()); removeSubtitleRenderer(); this.playerView = null; } @Override public boolean selectAudioTrack(PlayerAudioTrack audioTrack) throws IllegalStateException { return exoPlayer.selectAudioTrack(audioTrack); } @Override public boolean clearAudioTrackSelection() throws IllegalStateException { return exoPlayer.clearAudioTrackSelection(); } @Override public boolean showSubtitleTrack(PlayerSubtitleTrack subtitleTrack) throws IllegalStateException { setSubtitleRendererOutput(); playerView.showSubtitles(); return exoPlayer.selectSubtitleTrack(subtitleTrack); } private void setSubtitleRendererOutput() throws IllegalStateException { removeSubtitleRenderer(); textRendererOutput = new TextRendererOutput(playerView); exoPlayer.setSubtitleRendererOutput(textRendererOutput); } @Override public boolean hideSubtitleTrack() throws IllegalStateException { playerView.hideSubtitles(); removeSubtitleRenderer(); return exoPlayer.clearSubtitleTrackSelection(); } private void removeSubtitleRenderer() { if (textRendererOutput != null) { exoPlayer.removeSubtitleRendererOutput(textRendererOutput); } } @Override public AudioTracks getAudioTracks() throws IllegalStateException { return exoPlayer.getAudioTracks(); } @Override public boolean selectVideoTrack(PlayerVideoTrack videoTrack) throws IllegalStateException { return exoPlayer.selectVideoTrack(videoTrack); } @Override public Optional getSelectedVideoTrack() throws IllegalStateException { return exoPlayer.getSelectedVideoTrack(); } @Override public boolean clearVideoTrackSelection() throws IllegalStateException { return exoPlayer.clearVideoTrackSelection(); } @Override public List getVideoTracks() throws IllegalStateException { return exoPlayer.getVideoTracks(); } @Override public List getSubtitleTracks() throws IllegalStateException { return exoPlayer.getSubtitleTracks(); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/NoPlayerExoPlayerCreator.java ================================================ package com.novoda.noplayer.internal.exoplayer; import android.content.Context; import android.os.Handler; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.upstream.DataSource; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.internal.Heart; import com.novoda.noplayer.internal.SystemClock; import com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreator; import com.novoda.noplayer.internal.exoplayer.forwarder.ExoPlayerForwarder; import com.novoda.noplayer.internal.exoplayer.mediasource.MediaSourceFactory; import com.novoda.noplayer.internal.listeners.PlayerListenersHolder; import com.novoda.noplayer.internal.utils.AndroidDeviceVersion; import com.novoda.noplayer.internal.utils.Optional; import com.novoda.noplayer.model.LoadTimeout; public class NoPlayerExoPlayerCreator { private final InternalCreator internalCreator; public static NoPlayerExoPlayerCreator newInstance(String userAgent, Handler handler) { InternalCreator internalCreator = new InternalCreator(userAgent, handler, Optional.absent()); return new NoPlayerExoPlayerCreator(internalCreator); } public static NoPlayerExoPlayerCreator newInstance(String userAgent, Handler handler, DataSource.Factory dataSourceFactory) { InternalCreator internalCreator = new InternalCreator(userAgent, handler, Optional.of(dataSourceFactory)); return new NoPlayerExoPlayerCreator(internalCreator); } NoPlayerExoPlayerCreator(InternalCreator internalCreator) { this.internalCreator = internalCreator; } public NoPlayer createExoPlayer(Context context, DrmSessionCreator drmSessionCreator, boolean downgradeSecureDecoder, boolean allowCrossProtocolRedirects) { ExoPlayerTwoImpl player = internalCreator.create(context, drmSessionCreator, downgradeSecureDecoder, allowCrossProtocolRedirects); player.initialise(); return player; } static class InternalCreator { private final Handler handler; private final Optional dataSourceFactory; private final String userAgent; InternalCreator(String userAgent, Handler handler, Optional dataSourceFactory) { this.userAgent = userAgent; this.handler = handler; this.dataSourceFactory = dataSourceFactory; } ExoPlayerTwoImpl create(Context context, DrmSessionCreator drmSessionCreator, boolean downgradeSecureDecoder, boolean allowCrossProtocolRedirects) { MediaSourceFactory mediaSourceFactory = new MediaSourceFactory( context, userAgent, handler, dataSourceFactory, allowCrossProtocolRedirects ); MediaCodecSelector mediaCodecSelector = downgradeSecureDecoder ? SecurityDowngradingCodecSelector.newInstance() : MediaCodecSelector.DEFAULT_WITH_FALLBACK; CompositeTrackSelectorCreator trackSelectorCreator = new CompositeTrackSelectorCreator(); ExoPlayerCreator exoPlayerCreator = new ExoPlayerCreator(context); RendererTypeRequesterCreator rendererTypeRequesterCreator = new RendererTypeRequesterCreator(); AndroidDeviceVersion androidDeviceVersion = AndroidDeviceVersion.newInstance(); BandwidthMeterCreator bandwidthMeterCreator = new BandwidthMeterCreator(context); ExoPlayerFacade exoPlayerFacade = new ExoPlayerFacade( bandwidthMeterCreator, androidDeviceVersion, mediaSourceFactory, trackSelectorCreator, exoPlayerCreator, rendererTypeRequesterCreator ); PlayerListenersHolder listenersHolder = new PlayerListenersHolder(); ExoPlayerForwarder exoPlayerForwarder = new ExoPlayerForwarder(); LoadTimeout loadTimeout = new LoadTimeout(new SystemClock(), handler); Heart heart = Heart.newInstance(handler); return new ExoPlayerTwoImpl( exoPlayerFacade, listenersHolder, exoPlayerForwarder, loadTimeout, heart, drmSessionCreator, mediaCodecSelector ); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/RendererTypeRequester.java ================================================ package com.novoda.noplayer.internal.exoplayer; public interface RendererTypeRequester { int getRendererTypeFor(int index); } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/RendererTypeRequesterCreator.java ================================================ package com.novoda.noplayer.internal.exoplayer; import com.google.android.exoplayer2.SimpleExoPlayer; class RendererTypeRequesterCreator { RendererTypeRequester createfrom(final SimpleExoPlayer exoPlayer) { return new RendererTypeRequester() { @Override public int getRendererTypeFor(int index) { return exoPlayer.getRendererType(index); } }; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/SecurityDowngradingCodecSelector.java ================================================ package com.novoda.noplayer.internal.exoplayer; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import java.util.List; class SecurityDowngradingCodecSelector implements MediaCodecSelector { private static final boolean USE_INSECURE_DECODER = false; private final InternalMediaCodecUtil internalMediaCodecUtil; public static SecurityDowngradingCodecSelector newInstance() { InternalMediaCodecUtil internalMediaCodecUtil = new InternalMediaCodecUtil(); return new SecurityDowngradingCodecSelector(internalMediaCodecUtil); } SecurityDowngradingCodecSelector(InternalMediaCodecUtil internalMediaCodecUtil) { this.internalMediaCodecUtil = internalMediaCodecUtil; } @Override public List getDecoderInfos(String mimeType, boolean requiresSecureDecoder) throws MediaCodecUtil.DecoderQueryException { return internalMediaCodecUtil.getDecoderInfos(mimeType, USE_INSECURE_DECODER); } @Override public MediaCodecInfo getPassthroughDecoderInfo() throws MediaCodecUtil.DecoderQueryException { return internalMediaCodecUtil.getPassthroughDecoderInfo(); } static class InternalMediaCodecUtil { List getDecoderInfos(String mimeType, boolean requiresSecureDecoder) throws MediaCodecUtil.DecoderQueryException { return MediaCodecUtil.getDecoderInfos(mimeType, requiresSecureDecoder); } MediaCodecInfo getPassthroughDecoderInfo() throws MediaCodecUtil.DecoderQueryException { return MediaCodecUtil.getPassthroughDecoderInfo(); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/SimpleRenderersFactory.java ================================================ /* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.novoda.noplayer.internal.exoplayer; import android.content.Context; import android.os.Handler; import android.os.Looper; import android.support.annotation.IntDef; import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.text.SubtitleDecoder; import com.google.android.exoplayer2.text.SubtitleDecoderFactory; import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.text.TextRenderer; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.List; /** * Default {@link RenderersFactory} implementation. */ class SimpleRenderersFactory implements RenderersFactory { private static final boolean DO_NOT_PLAY_CLEAR_SAMPLES_WITHOUT_KEYS = false; private static final boolean INIT_ARGS = true; private static final boolean PLAY_CLEAR_SAMPLES_WITHOUT_KEYS = true; /** * Modes for using extension renderers. */ @Retention(RetentionPolicy.SOURCE) @IntDef({EXTENSION_RENDERER_MODE_OFF, EXTENSION_RENDERER_MODE_ON, EXTENSION_RENDERER_MODE_PREFER}) @interface ExtensionRendererMode { } /** * Do not allow use of extension renderers. */ static final int EXTENSION_RENDERER_MODE_OFF = 0; /** * Allow use of extension renderers. Extension renderers are indexed after core renderers of the * same type. A {@link TrackSelector} that prefers the first suitable renderer will therefore * prefer to use a core renderer to an extension renderer in the case that both are able to play * a given track. */ static final int EXTENSION_RENDERER_MODE_ON = 1; /** * Allow use of extension renderers. Extension renderers are indexed before core renderers of the * same type. A {@link TrackSelector} that prefers the first suitable renderer will therefore * prefer to use an extension renderer to a core renderer in the case that both are able to play * a given track. */ static final int EXTENSION_RENDERER_MODE_PREFER = 2; private static final String TAG = "DefaultRenderersFactory"; private static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50; private final Context context; @ExtensionRendererMode private final int extensionRendererMode; private final long allowedVideoJoiningTimeMs; private final MediaCodecSelector mediaCodecSelector; private final SubtitleDecoderFactory subtitleDecoderFactory; /** * @param context A {@link Context}. * @param extensionRendererMode The extension renderer mode, which determines if and how * available extension renderers are used. Note that extensions must be included in the * application build for them to be considered available. * @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt * to seamlessly join an ongoing playback. * @param mediaCodecSelector Used for selecting the codec for the video renderer. * @param subtitleDecoderFactory A factory from which to obtain {@link SubtitleDecoder} instances. */ SimpleRenderersFactory(Context context, @ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs, MediaCodecSelector mediaCodecSelector, SubtitleDecoderFactory subtitleDecoderFactory) { this.context = context; this.extensionRendererMode = extensionRendererMode; this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs; this.mediaCodecSelector = mediaCodecSelector; this.subtitleDecoderFactory = subtitleDecoderFactory; } @Override public Renderer[] createRenderers(Handler eventHandler, VideoRendererEventListener videoRendererEventListener, AudioRendererEventListener audioRendererEventListener, TextOutput textRendererOutput, MetadataOutput metadataRendererOutput, @Nullable DrmSessionManager drmSessionManager) { ArrayList renderersList = new ArrayList<>(); buildVideoRenderers(context, drmSessionManager, allowedVideoJoiningTimeMs, eventHandler, videoRendererEventListener, extensionRendererMode, renderersList); buildAudioRenderers(context, drmSessionManager, buildAudioProcessors(), eventHandler, audioRendererEventListener, extensionRendererMode, renderersList); buildTextRenderers(textRendererOutput, eventHandler.getLooper(), renderersList, subtitleDecoderFactory); buildMetadataRenderers(metadataRendererOutput, eventHandler.getLooper(), renderersList); buildMiscellaneousRenderers(); return renderersList.toArray(new Renderer[renderersList.size()]); } /** * Builds video renderers for use by the player. * * @param context The {@link Context} associated with the player. * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player * will not be used for DRM protected playbacks. * @param allowedVideoJoiningTimeMs The maximum duration in milliseconds for which video * renderers can attempt to seamlessly join an ongoing playback. * @param eventHandler A handler associated with the main thread's looper. * @param eventListener An event listener. * @param extensionRendererMode The extension renderer mode. * @param outRenderers An array to which the built renderers should be appended. */ @SuppressWarnings({"PMD.AvoidCatchingGenericException"}) // Using reflection and these APIs mean we need to do it private void buildVideoRenderers(Context context, DrmSessionManager drmSessionManager, long allowedVideoJoiningTimeMs, Handler eventHandler, VideoRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode, List outRenderers) { outRenderers.add(new MediaCodecVideoRenderer(context, mediaCodecSelector, allowedVideoJoiningTimeMs, drmSessionManager, DO_NOT_PLAY_CLEAR_SAMPLES_WITHOUT_KEYS, eventHandler, eventListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)); if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { return; } int extensionRendererIndex = outRenderers.size(); if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) { extensionRendererIndex--; } try { Class clazz = Class.forName("com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer"); Constructor constructor = clazz.getConstructor(boolean.class, long.class, Handler.class, VideoRendererEventListener.class, int.class); Renderer renderer = (Renderer) constructor.newInstance(INIT_ARGS, allowedVideoJoiningTimeMs, eventHandler, eventListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY); outRenderers.add(extensionRendererIndex, renderer); Log.i(TAG, "Loaded LibvpxVideoRenderer."); } catch (ClassNotFoundException e) { // Expected if the app was built without the extension. } catch (Exception e) { throw new RendererInstantiationException("LibvpxVideoRenderer", e); } } /** * Builds audio renderers for use by the player. * * @param context The {@link Context} associated with the player. * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player * will not be used for DRM protected playbacks. * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio * buffers before output. May be empty. * @param eventHandler A handler to use when invoking event listeners and outputs. * @param eventListener An event listener. * @param extensionRendererMode The extension renderer mode. * @param outRenderers An array to which the built renderers should be appended. */ @SuppressWarnings({"PMD.AvoidCatchingGenericException"}) // Using reflection and these APIs mean we need to do it private void buildAudioRenderers(Context context, DrmSessionManager drmSessionManager, AudioProcessor[] audioProcessors, Handler eventHandler, AudioRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode, List outRenderers) { MediaCodecAudioRenderer mediaCodecAudioRenderer = new MediaCodecAudioRenderer( context, mediaCodecSelector, drmSessionManager, PLAY_CLEAR_SAMPLES_WITHOUT_KEYS, eventHandler, eventListener, AudioCapabilities.getCapabilities(context), audioProcessors ); outRenderers.add(mediaCodecAudioRenderer); if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { return; } int extensionRendererIndex = outRenderers.size(); if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) { extensionRendererIndex--; } try { Class clazz = Class.forName("com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer"); Constructor constructor = clazz.getConstructor(Handler.class, AudioRendererEventListener.class, AudioProcessor[].class); Renderer renderer = (Renderer) constructor.newInstance(eventHandler, eventListener, audioProcessors); outRenderers.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded LibopusAudioRenderer."); } catch (ClassNotFoundException e) { // Expected if the app was built without the extension. } catch (Exception e) { throw new RendererInstantiationException("LibopusAudioRenderer", e); } try { Class clazz = Class.forName("com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer"); Constructor constructor = clazz.getConstructor(Handler.class, AudioRendererEventListener.class, AudioProcessor[].class); Renderer renderer = (Renderer) constructor.newInstance(eventHandler, eventListener, audioProcessors); outRenderers.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded LibflacAudioRenderer."); } catch (ClassNotFoundException e) { // Expected if the app was built without the extension. } catch (Exception e) { throw new RendererInstantiationException("LibflacAudioRenderer", e); } try { Class clazz = Class.forName("com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer"); Constructor constructor = clazz.getConstructor(Handler.class, AudioRendererEventListener.class, AudioProcessor[].class); Renderer renderer = (Renderer) constructor.newInstance(eventHandler, eventListener, audioProcessors); outRenderers.add(extensionRendererIndex, renderer); Log.i(TAG, "Loaded FfmpegAudioRenderer."); } catch (ClassNotFoundException e) { // Expected if the app was built without the extension. } catch (Exception e) { throw new RendererInstantiationException("FfmpegAudioRenderer", e); } } /** * Builds text renderers for use by the player. * @param output An output for the renderers. * @param outputLooper The looper associated with the thread on which the output should be * called. * @param outRenderers An array to which the built renderers should be appended. * @param decoderFactory A factory from which to obtain {@link SubtitleDecoder} instances. */ private void buildTextRenderers(TextOutput output, Looper outputLooper, List outRenderers, SubtitleDecoderFactory decoderFactory) { outRenderers.add(new TextRenderer(output, outputLooper, decoderFactory)); } /** * Builds metadata renderers for use by the player. * * @param output An output for the renderers. * @param outputLooper The looper associated with the thread on which the output should be * called. * @param outRenderers An array to which the built renderers should be appended. */ private void buildMetadataRenderers(MetadataOutput output, Looper outputLooper, List outRenderers) { outRenderers.add(new MetadataRenderer(output, outputLooper)); } /** * Builds any miscellaneous renderers used by the player. */ private void buildMiscellaneousRenderers() { // Do nothing. } /** * Builds an array of {@link AudioProcessor}s that will process PCM audio before output. */ private AudioProcessor[] buildAudioProcessors() { return new AudioProcessor[0]; } public static class RendererInstantiationException extends RuntimeException { RendererInstantiationException(String rendererName, Throwable cause) { super("Unable to instantiate renderer " + rendererName, cause); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/TextRendererOutput.java ================================================ package com.novoda.noplayer.internal.exoplayer; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextRenderer; import com.novoda.noplayer.PlayerView; import com.novoda.noplayer.model.TextCues; import java.util.List; class TextRendererOutput { private final PlayerView playerView; TextRendererOutput(PlayerView playerView) { this.playerView = playerView; } TextRenderer.Output output() { return new TextRenderer.Output() { @Override public void onCues(List cues) { TextCues textCues = ExoPlayerCueMapper.map(cues); playerView.setSubtitleCue(textCues); } }; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof TextRendererOutput)) { return false; } TextRendererOutput that = (TextRendererOutput) o; return playerView.equals(that.playerView); } @Override public int hashCode() { return playerView.hashCode(); } @Override public String toString() { return "TextRendererOutput{" + "playerView=" + playerView + '}'; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/drm/DownloadDrmSessionCreator.java ================================================ package com.novoda.noplayer.internal.exoplayer.drm; import android.os.Handler; import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.novoda.noplayer.drm.DownloadedModularDrm; class DownloadDrmSessionCreator implements DrmSessionCreator { private final DownloadedModularDrm downloadedModularDrm; private final FrameworkMediaDrmCreator mediaDrmCreator; private final Handler handler; DownloadDrmSessionCreator(DownloadedModularDrm downloadedModularDrm, FrameworkMediaDrmCreator mediaDrmCreator, Handler handler) { this.downloadedModularDrm = downloadedModularDrm; this.mediaDrmCreator = mediaDrmCreator; this.handler = handler; } @Override public DrmSessionManager create(DefaultDrmSessionEventListener eventListener) { return new LocalDrmSessionManager( downloadedModularDrm.getKeySetId(), mediaDrmCreator.create(WIDEVINE_MODULAR_UUID), WIDEVINE_MODULAR_UUID, handler, eventListener ); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/drm/DrmSessionCreator.java ================================================ package com.novoda.noplayer.internal.exoplayer.drm; import android.support.annotation.Nullable; import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import java.util.UUID; public interface DrmSessionCreator { UUID WIDEVINE_MODULAR_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL); @Nullable DrmSessionManager create(DefaultDrmSessionEventListener eventListener); } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/drm/DrmSessionCreatorException.java ================================================ package com.novoda.noplayer.internal.exoplayer.drm; import com.novoda.noplayer.drm.DrmType; public final class DrmSessionCreatorException extends Exception { static DrmSessionCreatorException noDrmHandlerFor(DrmType drmType) { return new DrmSessionCreatorException("No DrmHandler for DrmType: " + drmType); } private DrmSessionCreatorException(String message) { super(message); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/drm/DrmSessionCreatorFactory.java ================================================ package com.novoda.noplayer.internal.exoplayer.drm; import android.os.Build; import android.os.Handler; import com.novoda.noplayer.UnableToCreatePlayerException; import com.novoda.noplayer.drm.DownloadedModularDrm; import com.novoda.noplayer.drm.DrmHandler; import com.novoda.noplayer.drm.DrmType; import com.novoda.noplayer.drm.StreamingModularDrm; import com.novoda.noplayer.internal.drm.provision.ProvisionExecutor; import com.novoda.noplayer.internal.drm.provision.ProvisionExecutorCreator; import com.novoda.noplayer.internal.utils.AndroidDeviceVersion; public class DrmSessionCreatorFactory { private final AndroidDeviceVersion androidDeviceVersion; private final ProvisionExecutorCreator provisionExecutorCreator; private final Handler handler; public DrmSessionCreatorFactory(AndroidDeviceVersion androidDeviceVersion, ProvisionExecutorCreator provisionExecutorCreator, Handler handler) { this.androidDeviceVersion = androidDeviceVersion; this.provisionExecutorCreator = provisionExecutorCreator; this.handler = handler; } public DrmSessionCreator createFor(DrmType drmType, DrmHandler drmHandler) throws DrmSessionCreatorException { switch (drmType) { case NONE: // Fall-through. case WIDEVINE_CLASSIC: return new NoDrmSessionCreator(); case WIDEVINE_MODULAR_STREAM: assertThatApiLevelIsJellyBeanEighteenOrAbove(drmType); return createModularStream((StreamingModularDrm) drmHandler); case WIDEVINE_MODULAR_DOWNLOAD: assertThatApiLevelIsJellyBeanEighteenOrAbove(drmType); return createModularDownload((DownloadedModularDrm) drmHandler); default: throw DrmSessionCreatorException.noDrmHandlerFor(drmType); } } private void assertThatApiLevelIsJellyBeanEighteenOrAbove(DrmType drmType) { if (androidDeviceVersion.isJellyBeanEighteenOrAbove()) { return; } throw UnableToCreatePlayerException.deviceDoesNotMeetTargetApiException( drmType, Build.VERSION_CODES.JELLY_BEAN_MR2, androidDeviceVersion ); } private DrmSessionCreator createModularStream(StreamingModularDrm drmHandler) { ProvisionExecutor provisionExecutor = provisionExecutorCreator.create(); ProvisioningModularDrmCallback mediaDrmCallback = new ProvisioningModularDrmCallback( drmHandler, provisionExecutor ); FrameworkMediaDrmCreator mediaDrmCreator = new FrameworkMediaDrmCreator(); return new StreamingDrmSessionCreator(mediaDrmCallback, mediaDrmCreator, handler); } private DownloadDrmSessionCreator createModularDownload(DownloadedModularDrm drmHandler) { FrameworkMediaDrmCreator mediaDrmCreator = new FrameworkMediaDrmCreator(); return new DownloadDrmSessionCreator(drmHandler, mediaDrmCreator, handler); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/drm/FrameworkDrmSession.java ================================================ package com.novoda.noplayer.internal.exoplayer.drm; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; interface FrameworkDrmSession extends DrmSession { SessionId getSessionId(); } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/drm/FrameworkMediaDrmCreator.java ================================================ package com.novoda.noplayer.internal.exoplayer.drm; import com.google.android.exoplayer2.drm.FrameworkMediaDrm; import com.google.android.exoplayer2.drm.UnsupportedDrmException; import java.util.UUID; class FrameworkMediaDrmCreator { @SuppressWarnings("PMD.PreserveStackTrace") // We just unwrap the exception because we don't care about the UnsupportedDrmException itself FrameworkMediaDrm create(UUID uuid) { try { return FrameworkMediaDrm.newInstance(uuid); } catch (UnsupportedDrmException e) { throw new FrameworkMediaDrmException(e.getMessage(), e.getCause()); } } private static class FrameworkMediaDrmException extends RuntimeException { FrameworkMediaDrmException(String message, Throwable cause) { super(message, cause); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/drm/InvalidDrmSession.java ================================================ package com.novoda.noplayer.internal.exoplayer.drm; import android.support.annotation.Nullable; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import java.util.Map; class InvalidDrmSession implements FrameworkDrmSession { private static final byte[] ABSENT_OFFLINE_LICENSE_KEY_SET_ID = null; private final DrmSessionException drmSessionException; InvalidDrmSession(DrmSessionException drmSessionException) { this.drmSessionException = drmSessionException; } @Override public int getState() { return DrmSession.STATE_ERROR; } @Override public FrameworkMediaCrypto getMediaCrypto() { throw new IllegalStateException(); } @Override public DrmSessionException getError() { return drmSessionException; } @Override public Map queryKeyStatus() { throw new IllegalStateException(); } @SuppressWarnings("PMD.MethodReturnsInternalArray") // We return a constant null array @Override @Nullable public byte[] getOfflineLicenseKeySetId() { return ABSENT_OFFLINE_LICENSE_KEY_SET_ID; } @Override public SessionId getSessionId() { return SessionId.absent(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } InvalidDrmSession that = (InvalidDrmSession) o; return drmSessionException != null ? drmSessionException.equals(that.drmSessionException) : that.drmSessionException == null; } @Override public int hashCode() { return drmSessionException != null ? drmSessionException.hashCode() : 0; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/drm/LocalDrmSession.java ================================================ package com.novoda.noplayer.internal.exoplayer.drm; import android.support.annotation.Nullable; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.novoda.noplayer.model.KeySetId; import java.util.Collections; import java.util.HashMap; import java.util.Map; class LocalDrmSession implements FrameworkDrmSession { private static final DrmSessionException NO_EXCEPTION = null; private final FrameworkMediaCrypto mediaCrypto; private final KeySetId keySetIdToRestore; private final SessionId sessionId; LocalDrmSession(FrameworkMediaCrypto mediaCrypto, KeySetId keySetIdToRestore, SessionId sessionId) { this.mediaCrypto = mediaCrypto; this.keySetIdToRestore = keySetIdToRestore; this.sessionId = sessionId; } @Override public int getState() { return STATE_OPENED_WITH_KEYS; } @Override public FrameworkMediaCrypto getMediaCrypto() { return mediaCrypto; } @Nullable @Override public DrmSessionException getError() { return NO_EXCEPTION; } @Override public Map queryKeyStatus() { return Collections.unmodifiableMap(new HashMap()); } @Override public byte[] getOfflineLicenseKeySetId() { return keySetIdToRestore.asBytes(); } @Override public SessionId getSessionId() { return sessionId; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } LocalDrmSession that = (LocalDrmSession) o; if (mediaCrypto != null ? !mediaCrypto.equals(that.mediaCrypto) : that.mediaCrypto != null) { return false; } if (keySetIdToRestore != null ? !keySetIdToRestore.equals(that.keySetIdToRestore) : that.keySetIdToRestore != null) { return false; } return sessionId != null ? sessionId.equals(that.sessionId) : that.sessionId == null; } @Override public int hashCode() { int result = mediaCrypto != null ? mediaCrypto.hashCode() : 0; result = 31 * result + (keySetIdToRestore != null ? keySetIdToRestore.hashCode() : 0); result = 31 * result + (sessionId != null ? sessionId.hashCode() : 0); return result; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/drm/LocalDrmSessionManager.java ================================================ package com.novoda.noplayer.internal.exoplayer.drm; import android.annotation.TargetApi; import android.os.Build; import android.os.Handler; import android.os.Looper; import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaDrm; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.novoda.noplayer.model.KeySetId; import java.util.UUID; class LocalDrmSessionManager implements DrmSessionManager { private final KeySetId keySetIdToRestore; private final ExoMediaDrm mediaDrm; private final DefaultDrmSessionEventListener eventListener; private final UUID drmScheme; private final Handler handler; LocalDrmSessionManager(KeySetId keySetIdToRestore, ExoMediaDrm mediaDrm, UUID drmScheme, Handler handler, DefaultDrmSessionEventListener eventListener) { this.keySetIdToRestore = keySetIdToRestore; this.mediaDrm = mediaDrm; this.eventListener = eventListener; this.drmScheme = drmScheme; this.handler = handler; } @Override public boolean canAcquireSession(DrmInitData drmInitData) { DrmInitData.SchemeData schemeData = drmInitData.get(drmScheme); return schemeData != null; } @SuppressWarnings("PMD.AvoidCatchingGenericException") // We are forced to catch Exception as ResourceBusyException is minSdk 19 @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) @Override public DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitData) { DrmSession drmSession; try { SessionId sessionId = SessionId.of(mediaDrm.openSession()); FrameworkMediaCrypto mediaCrypto = mediaDrm.createMediaCrypto(sessionId.asBytes()); mediaDrm.restoreKeys(sessionId.asBytes(), keySetIdToRestore.asBytes()); drmSession = new LocalDrmSession(mediaCrypto, keySetIdToRestore, sessionId); } catch (Exception exception) { drmSession = new InvalidDrmSession(new DrmSession.DrmSessionException(exception)); notifyErrorListener(drmSession); } return drmSession; } private void notifyErrorListener(DrmSession drmSession) { final DrmSession.DrmSessionException error = drmSession.getError(); handler.post(new Runnable() { @Override public void run() { eventListener.onDrmSessionManagerError(error); } }); } @Override public void releaseSession(DrmSession drmSession) { FrameworkDrmSession frameworkDrmSession = (FrameworkDrmSession) drmSession; SessionId sessionId = frameworkDrmSession.getSessionId(); mediaDrm.closeSession(sessionId.asBytes()); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/drm/NoDrmSessionCreator.java ================================================ package com.novoda.noplayer.internal.exoplayer.drm; import android.support.annotation.Nullable; import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; class NoDrmSessionCreator implements DrmSessionCreator { private static final DrmSessionManager NO_DRM_SESSION = null; @Nullable @Override public DrmSessionManager create(DefaultDrmSessionEventListener eventListener) { return NO_DRM_SESSION; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/drm/ProvisioningModularDrmCallback.java ================================================ package com.novoda.noplayer.internal.exoplayer.drm; import com.google.android.exoplayer2.drm.ExoMediaDrm; import com.google.android.exoplayer2.drm.MediaDrmCallback; import com.novoda.noplayer.drm.ModularDrmKeyRequest; import com.novoda.noplayer.drm.ModularDrmProvisionRequest; import com.novoda.noplayer.drm.StreamingModularDrm; import com.novoda.noplayer.internal.drm.provision.ProvisionExecutor; import java.util.UUID; class ProvisioningModularDrmCallback implements MediaDrmCallback { private final StreamingModularDrm streamingModularDrm; private final ProvisionExecutor provisionExecutor; ProvisioningModularDrmCallback(StreamingModularDrm streamingModularDrm, ProvisionExecutor provisionExecutor) { this.streamingModularDrm = streamingModularDrm; this.provisionExecutor = provisionExecutor; } @Override public byte[] executeProvisionRequest(UUID uuid, ExoMediaDrm.ProvisionRequest request) throws Exception { return provisionExecutor.execute(new ModularDrmProvisionRequest(request.getDefaultUrl(), request.getData())); } @Override public byte[] executeKeyRequest(UUID uuid, ExoMediaDrm.KeyRequest request) throws Exception { return streamingModularDrm.executeKeyRequest(new ModularDrmKeyRequest(request.getLicenseServerUrl(), request.getData())); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/drm/SessionId.java ================================================ package com.novoda.noplayer.internal.exoplayer.drm; import java.util.Arrays; final class SessionId { private final byte[] sessionIdBytes; static SessionId absent() { return new SessionId(new byte[0]); } static SessionId of(byte[] sessionId) { return new SessionId(Arrays.copyOf(sessionId, sessionId.length)); } @SuppressWarnings("PMD.ArrayIsStoredDirectly") // This can only come from the factory methods, which do defensive copying private SessionId(byte[] sessionIdBytes) { this.sessionIdBytes = sessionIdBytes; } byte[] asBytes() { return Arrays.copyOf(sessionIdBytes, sessionIdBytes.length); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } SessionId sessionId = (SessionId) o; return Arrays.equals(sessionIdBytes, sessionId.sessionIdBytes); } @Override public int hashCode() { return Arrays.hashCode(sessionIdBytes); } @Override public String toString() { return "SessionId{" + "asBytes=" + Arrays.toString(sessionIdBytes) + '}'; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/drm/StreamingDrmSessionCreator.java ================================================ package com.novoda.noplayer.internal.exoplayer.drm; import android.os.Handler; import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaDrm; import com.google.android.exoplayer2.drm.MediaDrmCallback; import java.util.HashMap; class StreamingDrmSessionCreator implements DrmSessionCreator { @SuppressWarnings("PMD.LooseCoupling") // Unfortunately the DefaultDrmSessionManager takes a HashMap, not a Map private static final HashMap NO_OPTIONAL_PARAMETERS = null; private final MediaDrmCallback mediaDrmCallback; private final FrameworkMediaDrmCreator frameworkMediaDrmCreator; private final Handler handler; StreamingDrmSessionCreator(MediaDrmCallback mediaDrmCallback, FrameworkMediaDrmCreator frameworkMediaDrmCreator, Handler handler) { this.mediaDrmCallback = mediaDrmCallback; this.frameworkMediaDrmCreator = frameworkMediaDrmCreator; this.handler = handler; } @Override public DrmSessionManager create(DefaultDrmSessionEventListener eventListener) { FrameworkMediaDrm frameworkMediaDrm = frameworkMediaDrmCreator.create(WIDEVINE_MODULAR_UUID); DefaultDrmSessionManager defaultDrmSessionManager = new DefaultDrmSessionManager<>( WIDEVINE_MODULAR_UUID, frameworkMediaDrm, mediaDrmCallback, NO_OPTIONAL_PARAMETERS ); defaultDrmSessionManager.removeListener(eventListener); defaultDrmSessionManager.addListener(handler, eventListener); return defaultDrmSessionManager; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/error/ErrorFormatter.java ================================================ package com.novoda.noplayer.internal.exoplayer.error; import android.media.MediaCodec; import android.os.Build; import android.support.annotation.RequiresApi; final class ErrorFormatter { private ErrorFormatter() { // Static class. } static String formatMessage(Throwable throwable) { return throwable.getClass().getName() + ": " + throwable.getMessage(); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) static String formatCodecException(MediaCodec.CodecException exception) { String diagnosticInformation = "diagnosticInformation=" + exception.getDiagnosticInfo(); String isTransient = " : isTransient=" + exception.isTransient(); String isRecoverable = " : isRecoverable=" + exception.isRecoverable(); return diagnosticInformation + isTransient + isRecoverable; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/error/ExoPlayerErrorMapper.java ================================================ package com.novoda.noplayer.internal.exoplayer.error; import com.google.android.exoplayer2.ExoPlaybackException; import com.novoda.noplayer.DetailErrorType; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.NoPlayerError; import com.novoda.noplayer.PlayerErrorType; public final class ExoPlayerErrorMapper { private ExoPlayerErrorMapper() { // Static class. } public static NoPlayer.PlayerError errorFor(ExoPlaybackException exception) { String message = ErrorFormatter.formatMessage(exception.getCause()); switch (exception.type) { case ExoPlaybackException.TYPE_SOURCE: return SourceErrorMapper.map(exception.getSourceException(), message); case ExoPlaybackException.TYPE_RENDERER: return RendererErrorMapper.map(exception.getRendererException(), message); case ExoPlaybackException.TYPE_UNEXPECTED: return UnexpectedErrorMapper.map(exception.getUnexpectedException(), message); default: return new NoPlayerError(PlayerErrorType.UNKNOWN, DetailErrorType.UNKNOWN, message); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/error/RendererErrorMapper.java ================================================ package com.novoda.noplayer.internal.exoplayer.error; import android.media.MediaCodec; import com.google.android.exoplayer2.audio.AudioDecoderException; import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioSink; import com.google.android.exoplayer2.drm.DecryptionException; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.KeysExpiredException; import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import com.google.android.exoplayer2.text.SubtitleDecoderException; import com.novoda.noplayer.DetailErrorType; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.NoPlayerError; import com.novoda.noplayer.PlayerErrorType; final class RendererErrorMapper { private RendererErrorMapper() { // non-instantiable class } @SuppressWarnings({"PMD.StdCyclomaticComplexity", "PMD.CyclomaticComplexity", "PMD.ModifiedCyclomaticComplexity", "PMD.NPathComplexity"}) static NoPlayer.PlayerError map(Exception rendererException, String message) { if (rendererException instanceof AudioSink.ConfigurationException) { return new NoPlayerError(PlayerErrorType.RENDERER_DECODER, DetailErrorType.AUDIO_SINK_CONFIGURATION_ERROR, message); } if (rendererException instanceof AudioSink.InitializationException) { return new NoPlayerError(PlayerErrorType.RENDERER_DECODER, DetailErrorType.AUDIO_SINK_INITIALISATION_ERROR, message); } if (rendererException instanceof AudioSink.WriteException) { return new NoPlayerError(PlayerErrorType.RENDERER_DECODER, DetailErrorType.AUDIO_SINK_WRITE_ERROR, message); } if (rendererException instanceof AudioProcessor.UnhandledFormatException) { return new NoPlayerError(PlayerErrorType.RENDERER_DECODER, DetailErrorType.AUDIO_UNHANDLED_FORMAT_ERROR, message); } if (rendererException instanceof AudioDecoderException) { return new NoPlayerError(PlayerErrorType.RENDERER_DECODER, DetailErrorType.AUDIO_DECODER_ERROR, message); } if (rendererException instanceof MediaCodecRenderer.DecoderInitializationException) { MediaCodecRenderer.DecoderInitializationException decoderInitializationException = (MediaCodecRenderer.DecoderInitializationException) rendererException; String fullMessage = "decoder-name:" + decoderInitializationException.decoderName + ", " + "mimetype:" + decoderInitializationException.mimeType + ", " + "secureCodeRequired:" + decoderInitializationException.secureDecoderRequired + ", " + "diagnosticInfo:" + decoderInitializationException.diagnosticInfo + ", " + "exceptionMessage:" + message; return new NoPlayerError(PlayerErrorType.RENDERER_DECODER, DetailErrorType.INITIALISATION_ERROR, fullMessage); } if (rendererException instanceof MediaCodecUtil.DecoderQueryException) { return new NoPlayerError(PlayerErrorType.DEVICE_MEDIA_CAPABILITIES, DetailErrorType.UNKNOWN, message); } if (rendererException instanceof SubtitleDecoderException) { return new NoPlayerError(PlayerErrorType.RENDERER_DECODER, DetailErrorType.DECODING_SUBTITLE_ERROR, message); } if (rendererException instanceof UnsupportedDrmException) { return mapUnsupportedDrmException((UnsupportedDrmException) rendererException, message); } if (rendererException instanceof DefaultDrmSessionManager.MissingSchemeDataException) { return new NoPlayerError(PlayerErrorType.DRM, DetailErrorType.CANNOT_ACQUIRE_DRM_SESSION_MISSING_SCHEME_FOR_REQUIRED_UUID_ERROR, message); } if (rendererException instanceof DrmSession.DrmSessionException) { return new NoPlayerError(PlayerErrorType.DRM, DetailErrorType.DRM_SESSION_ERROR, message); } if (rendererException instanceof KeysExpiredException) { return new NoPlayerError(PlayerErrorType.DRM, DetailErrorType.DRM_KEYS_EXPIRED_ERROR, message); } if (rendererException instanceof DecryptionException) { return new NoPlayerError(PlayerErrorType.CONTENT_DECRYPTION, DetailErrorType.FAIL_DECRYPT_DATA_DUE_NON_PLATFORM_COMPONENT_ERROR, message); } if (rendererException instanceof MediaCodec.CryptoException) { return mapCryptoException((MediaCodec.CryptoException) rendererException, message); } if (rendererException instanceof IllegalStateException) { return new NoPlayerError(PlayerErrorType.DRM, DetailErrorType.MEDIA_REQUIRES_DRM_SESSION_MANAGER_ERROR, message); } return new NoPlayerError(PlayerErrorType.UNKNOWN, DetailErrorType.UNKNOWN, message); } private static NoPlayer.PlayerError mapUnsupportedDrmException(UnsupportedDrmException unsupportedDrmException, String message) { switch (unsupportedDrmException.reason) { case UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME: return new NoPlayerError(PlayerErrorType.DRM, DetailErrorType.UNSUPPORTED_DRM_SCHEME_ERROR, message); case UnsupportedDrmException.REASON_INSTANTIATION_ERROR: return new NoPlayerError(PlayerErrorType.DRM, DetailErrorType.DRM_INSTANTIATION_ERROR, message); default: return new NoPlayerError(PlayerErrorType.DRM, DetailErrorType.DRM_UNKNOWN_ERROR, message); } } private static NoPlayer.PlayerError mapCryptoException(MediaCodec.CryptoException cryptoException, String message) { switch (cryptoException.getErrorCode()) { case MediaCodec.CryptoException.ERROR_INSUFFICIENT_OUTPUT_PROTECTION: return new NoPlayerError(PlayerErrorType.CONTENT_DECRYPTION, DetailErrorType.INSUFFICIENT_OUTPUT_PROTECTION_ERROR, message); case MediaCodec.CryptoException.ERROR_KEY_EXPIRED: return new NoPlayerError(PlayerErrorType.CONTENT_DECRYPTION, DetailErrorType.KEY_EXPIRED_ERROR, message); case MediaCodec.CryptoException.ERROR_NO_KEY: return new NoPlayerError(PlayerErrorType.CONTENT_DECRYPTION, DetailErrorType.KEY_NOT_FOUND_WHEN_DECRYPTION_ERROR, message); case MediaCodec.CryptoException.ERROR_RESOURCE_BUSY: return new NoPlayerError(PlayerErrorType.CONTENT_DECRYPTION, DetailErrorType.RESOURCE_BUSY_ERROR_THEN_SHOULD_RETRY, message); case MediaCodec.CryptoException.ERROR_SESSION_NOT_OPENED: return new NoPlayerError(PlayerErrorType.CONTENT_DECRYPTION, DetailErrorType.ATTEMPTED_ON_CLOSED_SEDDION_ERROR, message); case MediaCodec.CryptoException.ERROR_UNSUPPORTED_OPERATION: return new NoPlayerError( PlayerErrorType.CONTENT_DECRYPTION, DetailErrorType.LICENSE_POLICY_REQUIRED_NOT_SUPPORTED_BY_DEVICE_ERROR, message ); default: return new NoPlayerError(PlayerErrorType.CONTENT_DECRYPTION, DetailErrorType.UNKNOWN, message); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/error/SourceErrorMapper.java ================================================ package com.novoda.noplayer.internal.exoplayer.error; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.offline.DownloadException; import com.google.android.exoplayer2.source.ClippingMediaSource; import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.dash.DashManifestStaleException; import com.google.android.exoplayer2.source.hls.SampleQueueMappingException; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; import com.google.android.exoplayer2.upstream.AssetDataSource; import com.google.android.exoplayer2.upstream.ContentDataSource; import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.FileDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.UdpDataSource; import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.util.PriorityTaskManager; import com.novoda.noplayer.DetailErrorType; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.NoPlayerError; import com.novoda.noplayer.PlayerErrorType; import java.io.IOException; final class SourceErrorMapper { private SourceErrorMapper() { // non-instantiable class } @SuppressWarnings({"PMD.StdCyclomaticComplexity", "PMD.CyclomaticComplexity", "PMD.ModifiedCyclomaticComplexity", "PMD.NPathComplexity"}) static NoPlayer.PlayerError map(IOException sourceException, String message) { if (sourceException instanceof SampleQueueMappingException) { return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.SAMPLE_QUEUE_MAPPING_ERROR, message); } if (sourceException instanceof FileDataSource.FileDataSourceException) { return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.READING_LOCAL_FILE_ERROR, message); } if (sourceException instanceof Loader.UnexpectedLoaderException) { return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.UNEXPECTED_LOADING_ERROR, message); } if (sourceException instanceof DashManifestStaleException) { return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.LIVE_STALE_MANIFEST_AND_NEW_MANIFEST_COULD_NOT_LOAD_ERROR, message); } if (sourceException instanceof DownloadException) { return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.DOWNLOAD_ERROR, message); } if (sourceException instanceof AdsMediaSource.AdLoadException) { return mapAdsError((AdsMediaSource.AdLoadException) sourceException, message); } if (sourceException instanceof MergingMediaSource.IllegalMergeException) { return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.MERGING_MEDIA_SOURCE_CANNOT_MERGE_ITS_SOURCES, message); } if (sourceException instanceof ClippingMediaSource.IllegalClippingException) { return mapClippingError((ClippingMediaSource.IllegalClippingException) sourceException, message); } if (sourceException instanceof PriorityTaskManager.PriorityTooLowException) { return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.TASK_CANNOT_PROCEED_PRIORITY_TOO_LOW, message); } if (sourceException instanceof ParserException) { return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.PARSING_MEDIA_DATA_OR_METADATA_ERROR, message); } if (sourceException instanceof Cache.CacheException) { return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.CACHE_WRITING_DATA_ERROR, message); } if (sourceException instanceof HlsPlaylistTracker.PlaylistStuckException) { HlsPlaylistTracker.PlaylistStuckException playlistStuckException = (HlsPlaylistTracker.PlaylistStuckException) sourceException; return new NoPlayerError( PlayerErrorType.CONNECTIVITY, DetailErrorType.HLS_PLAYLIST_STUCK_SERVER_SIDE_ERROR, playlistStuckException.url + " - " + message ); } if (sourceException instanceof HlsPlaylistTracker.PlaylistResetException) { HlsPlaylistTracker.PlaylistResetException playlistStuckException = (HlsPlaylistTracker.PlaylistResetException) sourceException; return new NoPlayerError( PlayerErrorType.CONNECTIVITY, DetailErrorType.HLS_PLAYLIST_SERVER_HAS_RESET, playlistStuckException.url + " - " + message ); } if (sourceException instanceof HttpDataSource.HttpDataSourceException) { return mapHttpDataSourceException((HttpDataSource.HttpDataSourceException) sourceException, message); } if (sourceException instanceof AssetDataSource.AssetDataSourceException) { return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.READ_LOCAL_ASSET_ERROR, message); } if (sourceException instanceof ContentDataSource.ContentDataSourceException) { return new NoPlayerError(PlayerErrorType.CONNECTIVITY, DetailErrorType.READ_CONTENT_URI_ERROR, message); } if (sourceException instanceof UdpDataSource.UdpDataSourceException) { return new NoPlayerError(PlayerErrorType.CONNECTIVITY, DetailErrorType.READ_FROM_UDP_ERROR, message); } if (sourceException instanceof DataSourceException) { return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.DATA_POSITION_OUT_OF_RANGE_ERROR, message); } return new NoPlayerError(PlayerErrorType.UNKNOWN, DetailErrorType.UNKNOWN, message); } private static NoPlayer.PlayerError mapAdsError(AdsMediaSource.AdLoadException adLoadException, String message) { switch (adLoadException.type) { case AdsMediaSource.AdLoadException.TYPE_AD: return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.AD_LOAD_ERROR_THEN_WILL_SKIP, message); case AdsMediaSource.AdLoadException.TYPE_AD_GROUP: return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.AD_GROUP_LOAD_ERROR_THEN_WILL_SKIP, message); case AdsMediaSource.AdLoadException.TYPE_ALL_ADS: return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.ALL_ADS_LOAD_ERROR_THEN_WILL_SKIP, message); case AdsMediaSource.AdLoadException.TYPE_UNEXPECTED: return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.ADS_LOAD_UNEXPECTED_ERROR_THEN_WILL_SKIP, message); default: return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.UNKNOWN, message); } } private static NoPlayer.PlayerError mapClippingError(ClippingMediaSource.IllegalClippingException illegalClippingException, String message) { switch (illegalClippingException.reason) { case ClippingMediaSource.IllegalClippingException.REASON_INVALID_PERIOD_COUNT: return new NoPlayerError( PlayerErrorType.SOURCE, DetailErrorType.CLIPPING_MEDIA_SOURCE_CANNOT_CLIP_WRAPPED_SOURCE_INVALID_PERIOD_COUNT, message ); case ClippingMediaSource.IllegalClippingException.REASON_NOT_SEEKABLE_TO_START: return new NoPlayerError( PlayerErrorType.SOURCE, DetailErrorType.CLIPPING_MEDIA_SOURCE_CANNOT_CLIP_WRAPPED_SOURCE_NOT_SEEKABLE_TO_START, message ); case ClippingMediaSource.IllegalClippingException.REASON_START_EXCEEDS_END: return new NoPlayerError( PlayerErrorType.SOURCE, DetailErrorType.CLIPPING_MEDIA_SOURCE_CANNOT_CLIP_WRAPPED_SOURCE_START_EXCEEDS_END, message ); default: return new NoPlayerError( PlayerErrorType.SOURCE, DetailErrorType.UNKNOWN, message ); } } private static NoPlayer.PlayerError mapHttpDataSourceException(HttpDataSource.HttpDataSourceException httpDataSourceException, String message) { switch (httpDataSourceException.type) { case HttpDataSource.HttpDataSourceException.TYPE_OPEN: return new NoPlayerError(PlayerErrorType.CONNECTIVITY, DetailErrorType.HTTP_CANNOT_OPEN_ERROR, message); case HttpDataSource.HttpDataSourceException.TYPE_READ: return new NoPlayerError(PlayerErrorType.CONNECTIVITY, DetailErrorType.HTTP_CANNOT_READ_ERROR, message); case HttpDataSource.HttpDataSourceException.TYPE_CLOSE: return new NoPlayerError(PlayerErrorType.CONNECTIVITY, DetailErrorType.HTTP_CANNOT_CLOSE_ERROR, message); default: return new NoPlayerError(PlayerErrorType.CONNECTIVITY, DetailErrorType.UNKNOWN, message); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/error/UnexpectedErrorMapper.java ================================================ package com.novoda.noplayer.internal.exoplayer.error; import android.media.MediaCodec; import android.os.Build; import com.google.android.exoplayer2.audio.DefaultAudioSink; import com.google.android.exoplayer2.util.EGLSurfaceTexture; import com.novoda.noplayer.DetailErrorType; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.NoPlayerError; import com.novoda.noplayer.PlayerErrorType; final class UnexpectedErrorMapper { private UnexpectedErrorMapper() { // non-instantiable class } static NoPlayer.PlayerError map(RuntimeException unexpectedException, String message) { if (unexpectedException instanceof EGLSurfaceTexture.GlException) { return new NoPlayerError(PlayerErrorType.UNEXPECTED, DetailErrorType.EGL_OPERATION_ERROR, message); } if (unexpectedException instanceof DefaultAudioSink.InvalidAudioTrackTimestampException) { return new NoPlayerError(PlayerErrorType.UNEXPECTED, DetailErrorType.SPURIOUS_AUDIO_TRACK_TIMESTAMP_ERROR, message); } if (unexpectedException instanceof IllegalStateException && message.contains("Multiple renderer media clocks")) { return new NoPlayerError(PlayerErrorType.UNEXPECTED, DetailErrorType.MULTIPLE_RENDERER_MEDIA_CLOCK_ENABLED_ERROR, message); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && unexpectedException instanceof MediaCodec.CodecException) { String errorMessage = ErrorFormatter.formatCodecException((MediaCodec.CodecException) unexpectedException); return new NoPlayerError(PlayerErrorType.UNEXPECTED, DetailErrorType.UNEXPECTED_CODEC_ERROR, errorMessage); } return new NoPlayerError(PlayerErrorType.UNKNOWN, DetailErrorType.UNKNOWN, message); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/AnalyticsListenerForwarder.java ================================================ package com.novoda.noplayer.internal.exoplayer.forwarder; import android.view.Surface; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.novoda.noplayer.NoPlayer; import java.io.IOException; import java.util.HashMap; import static com.novoda.noplayer.internal.exoplayer.forwarder.ForwarderInformation.Methods; import static com.novoda.noplayer.internal.exoplayer.forwarder.ForwarderInformation.Parameters; class AnalyticsListenerForwarder implements AnalyticsListener { private final NoPlayer.InfoListener infoListeners; AnalyticsListenerForwarder(NoPlayer.InfoListener infoListeners) { this.infoListeners = infoListeners; } @Override public void onPlayerStateChanged(EventTime eventTime, boolean playWhenReady, int playbackState) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.PLAY_WHEN_READY, String.valueOf(playWhenReady)); callingMethodParameters.put(Parameters.PLAYBACK_STATE, String.valueOf(playbackState)); infoListeners.onNewInfo(Methods.ON_PLAYER_STATE_CHANGED, callingMethodParameters); } @Override public void onTimelineChanged(EventTime eventTime, int reason) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.REASON, String.valueOf(reason)); infoListeners.onNewInfo(Methods.ON_TIMELINE_CHANGED, callingMethodParameters); } @Override public void onPositionDiscontinuity(EventTime eventTime, int reason) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.REASON, String.valueOf(reason)); infoListeners.onNewInfo(Methods.ON_POSITION_DISCONTINUITY, callingMethodParameters); } @Override public void onSeekStarted(EventTime eventTime) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); infoListeners.onNewInfo(Methods.ON_SEEK_STARTED, callingMethodParameters); } @Override public void onSeekProcessed(EventTime eventTime) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); infoListeners.onNewInfo(Methods.ON_SEEK_PROCESSED, callingMethodParameters); } @Override public void onPlaybackParametersChanged(EventTime eventTime, PlaybackParameters playbackParameters) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.PLAYBACK_PARAMETERS, playbackParameters.toString()); infoListeners.onNewInfo(Methods.ON_PLAYBACK_PARAMETERS_CHANGED, callingMethodParameters); } @Override public void onRepeatModeChanged(EventTime eventTime, int repeatMode) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.REPEAT_MODE, String.valueOf(repeatMode)); infoListeners.onNewInfo(Methods.ON_REPEAT_MODE_CHANGED, callingMethodParameters); } @Override public void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.SHUFFLE_MODE_ENABLED, String.valueOf(shuffleModeEnabled)); infoListeners.onNewInfo(Methods.ON_SHUFFLE_MODE_CHANGED, callingMethodParameters); } @Override public void onLoadingChanged(EventTime eventTime, boolean isLoading) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.IS_LOADING, String.valueOf(isLoading)); infoListeners.onNewInfo(Methods.ON_LOADING_CHANGED, callingMethodParameters); } @Override public void onPlayerError(EventTime eventTime, ExoPlaybackException error) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.ERROR, error.toString()); infoListeners.onNewInfo(Methods.ON_PLAYER_ERROR, callingMethodParameters); } @Override public void onTracksChanged(EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.TRACK_GROUPS, trackGroups.toString()); callingMethodParameters.put(Parameters.TRACK_SELECTIONS, trackSelections.toString()); infoListeners.onNewInfo(Methods.ON_TRACKS_CHANGED, callingMethodParameters); } @Override public void onLoadStarted(EventTime eventTime, MediaSourceEventListener.LoadEventInfo loadEventInfo, MediaSourceEventListener.MediaLoadData mediaLoadData) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.LOAD_EVENT_INFO, loadEventInfo.toString()); callingMethodParameters.put(Parameters.MEDIA_LOAD_DATA, mediaLoadData.toString()); infoListeners.onNewInfo(Methods.ON_LOAD_STARTED, callingMethodParameters); } @Override public void onLoadCompleted(EventTime eventTime, MediaSourceEventListener.LoadEventInfo loadEventInfo, MediaSourceEventListener.MediaLoadData mediaLoadData) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.LOAD_EVENT_INFO, loadEventInfo.toString()); callingMethodParameters.put(Parameters.MEDIA_LOAD_DATA, mediaLoadData.toString()); infoListeners.onNewInfo(Methods.ON_LOAD_COMPLETED, callingMethodParameters); } @Override public void onLoadCanceled(EventTime eventTime, MediaSourceEventListener.LoadEventInfo loadEventInfo, MediaSourceEventListener.MediaLoadData mediaLoadData) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.LOAD_EVENT_INFO, loadEventInfo.toString()); callingMethodParameters.put(Parameters.MEDIA_LOAD_DATA, mediaLoadData.toString()); infoListeners.onNewInfo(Methods.ON_LOAD_CANCELED, callingMethodParameters); } @Override public void onLoadError(EventTime eventTime, MediaSourceEventListener.LoadEventInfo loadEventInfo, MediaSourceEventListener.MediaLoadData mediaLoadData, IOException error, boolean wasCanceled) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.LOAD_EVENT_INFO, loadEventInfo.toString()); callingMethodParameters.put(Parameters.MEDIA_LOAD_DATA, mediaLoadData.toString()); callingMethodParameters.put(Parameters.ERROR, error.toString()); callingMethodParameters.put(Parameters.WAS_CANCELED, String.valueOf(wasCanceled)); infoListeners.onNewInfo(Methods.ON_LOAD_ERROR, callingMethodParameters); } @Override public void onDownstreamFormatChanged(EventTime eventTime, MediaSourceEventListener.MediaLoadData mediaLoadData) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.MEDIA_LOAD_DATA, mediaLoadData.toString()); infoListeners.onNewInfo(Methods.ON_DOWNSTREAM_FORMAT_CHANGED, callingMethodParameters); } @Override public void onUpstreamDiscarded(EventTime eventTime, MediaSourceEventListener.MediaLoadData mediaLoadData) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.MEDIA_LOAD_DATA, mediaLoadData.toString()); infoListeners.onNewInfo(Methods.ON_UPSTREAM_DISCARDED, callingMethodParameters); } @Override public void onMediaPeriodCreated(EventTime eventTime) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); infoListeners.onNewInfo(Methods.ON_MEDIA_PERIOD_CREATED, callingMethodParameters); } @Override public void onMediaPeriodReleased(EventTime eventTime) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); infoListeners.onNewInfo(Methods.ON_MEDIA_PERIOD_RELEASED, callingMethodParameters); } @Override public void onReadingStarted(EventTime eventTime) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); infoListeners.onNewInfo(Methods.ON_READING_STARTED, callingMethodParameters); } @Override public void onBandwidthEstimate(EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.TOTAL_LOAD_TIME_MS, String.valueOf(totalLoadTimeMs)); callingMethodParameters.put(Parameters.TOTAL_BYTES_LOADED, String.valueOf(totalBytesLoaded)); callingMethodParameters.put(Parameters.BITRATE_ESTIMATE, String.valueOf(bitrateEstimate)); infoListeners.onNewInfo(Methods.ON_BANDWIDTH_ESTIMATE, callingMethodParameters); } @Override public void onSurfaceSizeChanged(EventTime eventTime, int width, int height) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.WIDTH, String.valueOf(width)); callingMethodParameters.put(Parameters.HEIGHT, String.valueOf(height)); infoListeners.onNewInfo(Methods.ON_SURFACE_SIZE_CHANGED, callingMethodParameters); } @Override public void onMetadata(EventTime eventTime, Metadata metadata) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.METADATA, metadata.toString()); infoListeners.onNewInfo(Methods.ON_METADATA, callingMethodParameters); } @Override public void onDecoderEnabled(EventTime eventTime, int trackType, DecoderCounters decoderCounters) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.TRACK_TYPE, String.valueOf(trackType)); callingMethodParameters.put(Parameters.DECODER_COUNTERS, decoderCounters.toString()); infoListeners.onNewInfo(Methods.ON_DECODER_ENABLED, callingMethodParameters); } @Override public void onDecoderInitialized(EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.TRACK_TYPE, String.valueOf(trackType)); callingMethodParameters.put(Parameters.DECODER_NAME, decoderName); callingMethodParameters.put(Parameters.INITIALIZATION_DURATION_MS, String.valueOf(initializationDurationMs)); infoListeners.onNewInfo(Methods.ON_DECODER_INITIALIZED, callingMethodParameters); } @Override public void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.TRACK_TYPE, String.valueOf(trackType)); callingMethodParameters.put(Parameters.FORMAT, format.toString()); infoListeners.onNewInfo(Methods.ON_DECODER_INPUT_FORMAT_CHANGED, callingMethodParameters); } @Override public void onDecoderDisabled(EventTime eventTime, int trackType, DecoderCounters decoderCounters) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.TRACK_TYPE, String.valueOf(trackType)); callingMethodParameters.put(Parameters.DECODER_COUNTERS, decoderCounters.toString()); infoListeners.onNewInfo(Methods.ON_DECODER_DISABLED, callingMethodParameters); } @Override public void onAudioSessionId(EventTime eventTime, int audioSessionId) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.AUDIO_SESSION_ID, String.valueOf(audioSessionId)); infoListeners.onNewInfo(Methods.ON_AUDIO_SESSION_ID, callingMethodParameters); } @Override public void onAudioUnderrun(EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.BUFFER_SIZE, String.valueOf(bufferSize)); callingMethodParameters.put(Parameters.BUFFER_SIZE_MS, String.valueOf(bufferSizeMs)); callingMethodParameters.put(Parameters.ELAPSED_SINCE_LAST_FEED_MS, String.valueOf(elapsedSinceLastFeedMs)); infoListeners.onNewInfo(Methods.ON_AUDIO_UNDERRUN, callingMethodParameters); } @Override public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.DROPPED_FRAMES, String.valueOf(droppedFrames)); callingMethodParameters.put(Parameters.ELAPSED_MS, String.valueOf(elapsedMs)); infoListeners.onNewInfo(Methods.ON_DROPPED_VIDEO_FRAMES, callingMethodParameters); } @Override public void onVideoSizeChanged(EventTime eventTime, int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.WIDTH, String.valueOf(width)); callingMethodParameters.put(Parameters.HEIGHT, String.valueOf(height)); callingMethodParameters.put(Parameters.UNAPPLIED_ROTATION_DEGREES, String.valueOf(unappliedRotationDegrees)); callingMethodParameters.put(Parameters.PIXEL_WIDTH_HEIGHT_RATIO, String.valueOf(pixelWidthHeightRatio)); infoListeners.onNewInfo(Methods.ON_VIDEO_SIZE_CHANGED, callingMethodParameters); } @Override public void onRenderedFirstFrame(EventTime eventTime, Surface surface) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.SURFACE, surface.toString()); infoListeners.onNewInfo(Methods.ON_RENDERED_FIRST_FRAME, callingMethodParameters); } @Override public void onDrmKeysLoaded(EventTime eventTime) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); infoListeners.onNewInfo(Methods.ON_DRM_KEYS_LOADED, callingMethodParameters); } @Override public void onDrmSessionManagerError(EventTime eventTime, Exception error) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); callingMethodParameters.put(Parameters.ERROR, error.toString()); infoListeners.onNewInfo(Methods.ON_DRM_SESSION_MANAGER_ERROR, callingMethodParameters); } @Override public void onDrmKeysRestored(EventTime eventTime) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); infoListeners.onNewInfo(Methods.ON_DRM_KEYS_RESTORED, callingMethodParameters); } @Override public void onDrmKeysRemoved(EventTime eventTime) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString()); infoListeners.onNewInfo(Methods.ON_DRM_KEYS_REMOVED, callingMethodParameters); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/BitrateForwarder.java ================================================ package com.novoda.noplayer.internal.exoplayer.forwarder; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.model.Bitrate; import java.io.IOException; class BitrateForwarder implements MediaSourceEventListener { private Bitrate videoBitrate = Bitrate.fromBitsPerSecond(0); private Bitrate audioBitrate = Bitrate.fromBitsPerSecond(0); private final NoPlayer.BitrateChangedListener bitrateChangedListener; BitrateForwarder(NoPlayer.BitrateChangedListener bitrateChangedListener) { this.bitrateChangedListener = bitrateChangedListener; } @Override public void onMediaPeriodCreated(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { // TODO: should we send? } @Override public void onMediaPeriodReleased(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { // TODO: should we send? } @Override public void onLoadStarted(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { // TODO: should we send? } @Override public void onLoadCompleted(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { // TODO: should we send? } @Override public void onLoadCanceled(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { // TODO: should we send? } @Override public void onLoadError(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData, IOException error, boolean wasCanceled) { // TODO: should we send? } @Override public void onReadingStarted(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { // TODO: should we send? } @Override public void onUpstreamDiscarded(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { // TODO: should we send? } @Override public void onDownstreamFormatChanged(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { if (mediaLoadData.trackFormat == null) { return; } if (mediaLoadData.trackType == C.TRACK_TYPE_VIDEO) { videoBitrate = Bitrate.fromBitsPerSecond(mediaLoadData.trackFormat.bitrate); bitrateChangedListener.onBitrateChanged(audioBitrate, videoBitrate); } else if (mediaLoadData.trackType == C.TRACK_TYPE_AUDIO) { audioBitrate = Bitrate.fromBitsPerSecond(mediaLoadData.trackFormat.bitrate); bitrateChangedListener.onBitrateChanged(audioBitrate, videoBitrate); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/BufferStateForwarder.java ================================================ package com.novoda.noplayer.internal.exoplayer.forwarder; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.novoda.noplayer.NoPlayer; class BufferStateForwarder implements Player.EventListener { private final NoPlayer.BufferStateListener bufferStateListener; BufferStateForwarder(NoPlayer.BufferStateListener bufferStateListener) { this.bufferStateListener = bufferStateListener; } @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { if (playbackState == Player.STATE_BUFFERING) { bufferStateListener.onBufferStarted(); } else if (playbackState == Player.STATE_READY) { bufferStateListener.onBufferCompleted(); } } @Override public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { // TODO: should we send? } @Override public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { // TODO: should we send? } @Override public void onTimelineChanged(Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { // TODO: should we send? } @Override public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { // TODO: should we send? } @Override public void onLoadingChanged(boolean isLoading) { // TODO: should we send? } @Override public void onPlayerError(ExoPlaybackException error) { // Sent by ErrorForwarder. } @Override public void onPositionDiscontinuity(int reason) { // TODO: should we send? } @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { // TODO: should we send? } @Override public void onSeekProcessed() { // TODO: should we send? } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/DrmSessionInfoForwarder.java ================================================ package com.novoda.noplayer.internal.exoplayer.forwarder; import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener; import com.novoda.noplayer.NoPlayer; import java.util.Collections; import java.util.HashMap; import static com.novoda.noplayer.internal.exoplayer.forwarder.ForwarderInformation.Methods; import static com.novoda.noplayer.internal.exoplayer.forwarder.ForwarderInformation.Parameters; class DrmSessionInfoForwarder implements DefaultDrmSessionEventListener { private final NoPlayer.InfoListener infoListener; DrmSessionInfoForwarder(NoPlayer.InfoListener infoListener) { this.infoListener = infoListener; } @Override public void onDrmKeysLoaded() { infoListener.onNewInfo(Methods.ON_DRM_KEYS_LOADED, Collections.emptyMap()); } @Override public void onDrmSessionManagerError(Exception error) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.ERROR, String.valueOf(error)); infoListener.onNewInfo(Methods.ON_DRM_SESSION_MANAGER_ERROR, callingMethodParameters); } @Override public void onDrmKeysRestored() { infoListener.onNewInfo(Methods.ON_DRM_KEYS_RESTORED, Collections.emptyMap()); } @Override public void onDrmKeysRemoved() { infoListener.onNewInfo(Methods.ON_DRM_KEYS_REMOVED, Collections.emptyMap()); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/EventInfoForwarder.java ================================================ package com.novoda.noplayer.internal.exoplayer.forwarder; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.novoda.noplayer.NoPlayer; import java.util.Collections; import java.util.HashMap; import static com.novoda.noplayer.internal.exoplayer.forwarder.ForwarderInformation.Methods; import static com.novoda.noplayer.internal.exoplayer.forwarder.ForwarderInformation.Parameters; class EventInfoForwarder implements Player.EventListener { private final NoPlayer.InfoListener infoListener; EventInfoForwarder(NoPlayer.InfoListener infoListener) { this.infoListener = infoListener; } @Override public void onTimelineChanged(Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.TIMELINE, String.valueOf(timeline)); callingMethodParameters.put(Parameters.MANIFEST, String.valueOf(manifest)); callingMethodParameters.put(Parameters.REASON, String.valueOf(reason)); infoListener.onNewInfo(Methods.ON_TIMELINE_CHANGED, callingMethodParameters); } @Override public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.TRACK_GROUPS, String.valueOf(trackGroups)); callingMethodParameters.put(Parameters.TRACK_SELECTIONS, String.valueOf(trackSelections)); infoListener.onNewInfo(Methods.ON_TRACKS_CHANGED, callingMethodParameters); } @Override public void onLoadingChanged(boolean isLoading) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.IS_LOADING, String.valueOf(isLoading)); infoListener.onNewInfo(Methods.ON_LOADING_CHANGED, callingMethodParameters); } @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.PLAY_WHEN_READY, String.valueOf(playWhenReady)); callingMethodParameters.put(Parameters.PLAYBACK_STATE, String.valueOf(playbackState)); infoListener.onNewInfo(Methods.ON_PLAYER_STATE_CHANGED, callingMethodParameters); } @Override public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.REPEAT_MODE, String.valueOf(repeatMode)); infoListener.onNewInfo(Methods.ON_REPEAT_MODE_CHANGED, callingMethodParameters); } @Override public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.SHUFFLE_MODE_ENABLED, String.valueOf(shuffleModeEnabled)); infoListener.onNewInfo(Methods.ON_SHUFFLE_MODE_ENABLED_CHANGED, callingMethodParameters); } @Override public void onPlayerError(ExoPlaybackException error) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.ERROR, String.valueOf(error)); infoListener.onNewInfo(Methods.ON_PLAYER_ERROR, callingMethodParameters); } @Override public void onPositionDiscontinuity(int reason) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.REASON, String.valueOf(reason)); infoListener.onNewInfo(Methods.ON_POSITION_DISCONTINUITY, callingMethodParameters); } @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.PLAYBACK_PARAMETERS, String.valueOf(playbackParameters)); infoListener.onNewInfo(Methods.ON_PLAYBACK_PARAMETERS_CHANGED, callingMethodParameters); } @Override public void onSeekProcessed() { infoListener.onNewInfo(Methods.ON_POSITION_DISCONTINUITY, Collections.emptyMap()); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/EventListener.java ================================================ package com.novoda.noplayer.internal.exoplayer.forwarder; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; class EventListener implements Player.EventListener { private final List listeners = new CopyOnWriteArrayList<>(); public void add(Player.EventListener listener) { listeners.add(listener); } @Override public void onTimelineChanged(Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { for (Player.EventListener listener : listeners) { listener.onTimelineChanged(timeline, manifest, reason); } } @Override public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { for (Player.EventListener listener : listeners) { listener.onTracksChanged(trackGroups, trackSelections); } } @Override public void onLoadingChanged(boolean isLoading) { for (Player.EventListener listener : listeners) { listener.onLoadingChanged(isLoading); } } @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { for (Player.EventListener listener : listeners) { listener.onPlayerStateChanged(playWhenReady, playbackState); } } @Override public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { for (Player.EventListener listener : listeners) { listener.onRepeatModeChanged(repeatMode); } } @Override public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { for (Player.EventListener listener : listeners) { listener.onShuffleModeEnabledChanged(shuffleModeEnabled); } } @Override public void onPlayerError(ExoPlaybackException error) { for (Player.EventListener listener : listeners) { listener.onPlayerError(error); } } @Override public void onPositionDiscontinuity(int reason) { for (Player.EventListener listener : listeners) { listener.onPositionDiscontinuity(reason); } } @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { for (Player.EventListener listener : listeners) { listener.onPlaybackParametersChanged(playbackParameters); } } @Override public void onSeekProcessed() { for (Player.EventListener listener : listeners) { listener.onSeekProcessed(); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/ExoPlayerDrmSessionEventListener.java ================================================ package com.novoda.noplayer.internal.exoplayer.forwarder; import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; class ExoPlayerDrmSessionEventListener implements DefaultDrmSessionEventListener { private final List listeners = new CopyOnWriteArrayList<>(); void add(DefaultDrmSessionEventListener listener) { listeners.add(listener); } @Override public void onDrmKeysLoaded() { for (DefaultDrmSessionEventListener listener : listeners) { listener.onDrmKeysLoaded(); } } @Override public void onDrmSessionManagerError(Exception e) { for (DefaultDrmSessionEventListener listener : listeners) { listener.onDrmSessionManagerError(e); } } @Override public void onDrmKeysRestored() { for (DefaultDrmSessionEventListener listener : listeners) { listener.onDrmKeysRestored(); } } @Override public void onDrmKeysRemoved() { for (DefaultDrmSessionEventListener listener : listeners) { listener.onDrmKeysRemoved(); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/ExoPlayerForwarder.java ================================================ package com.novoda.noplayer.internal.exoplayer.forwarder; import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.video.VideoListener; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.PlayerState; public class ExoPlayerForwarder { private final EventListener exoPlayerEventListener; private final NoPlayerMediaSourceEventListener mediaSourceEventListener; private final NoPlayerAnalyticsListener analyticsListener; private final ExoPlayerVideoListener videoListener; private final ExoPlayerDrmSessionEventListener drmSessionEventListener; public ExoPlayerForwarder() { exoPlayerEventListener = new EventListener(); mediaSourceEventListener = new NoPlayerMediaSourceEventListener(); videoListener = new ExoPlayerVideoListener(); analyticsListener = new NoPlayerAnalyticsListener(); drmSessionEventListener = new ExoPlayerDrmSessionEventListener(); } public EventListener exoPlayerEventListener() { return exoPlayerEventListener; } public MediaSourceEventListener mediaSourceEventListener() { return mediaSourceEventListener; } public VideoListener videoListener() { return videoListener; } public DefaultDrmSessionEventListener drmSessionEventListener() { return drmSessionEventListener; } public AnalyticsListener analyticsListener() { return analyticsListener; } public void bind(NoPlayer.PreparedListener preparedListener, PlayerState playerState) { exoPlayerEventListener.add(new OnPrepareForwarder(preparedListener, playerState)); } public void bind(NoPlayer.CompletionListener completionListener, NoPlayer.StateChangedListener stateChangedListener) { exoPlayerEventListener.add(new OnCompletionForwarder(completionListener)); exoPlayerEventListener.add(new OnCompletionStateChangedForwarder(stateChangedListener)); } public void bind(NoPlayer.ErrorListener errorListener) { exoPlayerEventListener.add(new PlayerOnErrorForwarder(errorListener)); } public void bind(NoPlayer.BufferStateListener bufferStateListener) { exoPlayerEventListener.add(new BufferStateForwarder(bufferStateListener)); } public void bind(NoPlayer.VideoSizeChangedListener videoSizeChangedListener) { videoListener.add(new VideoSizeChangedForwarder(videoSizeChangedListener)); } public void bind(NoPlayer.BitrateChangedListener bitrateChangedListener) { mediaSourceEventListener.add(new BitrateForwarder(bitrateChangedListener)); } public void bind(NoPlayer.InfoListener infoListeners) { exoPlayerEventListener.add(new EventInfoForwarder(infoListeners)); mediaSourceEventListener.add(new MediaSourceEventForwarder(infoListeners)); drmSessionEventListener.add(new DrmSessionInfoForwarder(infoListeners)); analyticsListener.add(new AnalyticsListenerForwarder(infoListeners)); } public void bind(NoPlayer.DroppedVideoFramesListener droppedVideoFramesListeners) { analyticsListener.add(droppedVideoFramesListeners); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/ExoPlayerVideoListener.java ================================================ package com.novoda.noplayer.internal.exoplayer.forwarder; import com.google.android.exoplayer2.video.VideoListener; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; class ExoPlayerVideoListener implements VideoListener { private final List listeners = new CopyOnWriteArrayList<>(); public void add(VideoListener listener) { listeners.add(listener); } @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { for (VideoListener listener : listeners) { listener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); } } @Override public void onRenderedFirstFrame() { for (VideoListener listener : listeners) { listener.onRenderedFirstFrame(); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/ForwarderInformation.java ================================================ package com.novoda.noplayer.internal.exoplayer.forwarder; class ForwarderInformation { static final class Parameters { private Parameters() { // non-instantiable } static final String EVENT_TIME = "eventTime"; static final String PLAY_WHEN_READY = "playWhenReady"; static final String PLAYBACK_STATE = "playbackState"; static final String REASON = "reason"; static final String PLAYBACK_PARAMETERS = "playbackParameters"; static final String REPEAT_MODE = "repeatMode"; static final String SHUFFLE_MODE_ENABLED = "shuffleModeEnabled"; static final String IS_LOADING = "isLoading"; static final String ERROR = "error"; static final String TRACK_GROUPS = "trackGroups"; static final String TRACK_SELECTIONS = "trackSelections"; static final String LOAD_EVENT_INFO = "loadEventInfo"; static final String MEDIA_LOAD_DATA = "mediaLoadData"; static final String WAS_CANCELED = "wasCanceled"; static final String TOTAL_LOAD_TIME_MS = "totalLoadTimeMs"; static final String TOTAL_BYTES_LOADED = "totalBytesLoaded"; static final String BITRATE_ESTIMATE = "bitrateEstimate"; static final String WIDTH = "width"; static final String HEIGHT = "height"; static final String METADATA = "metadata"; static final String TRACK_TYPE = "trackType"; static final String DECODER_COUNTERS = "decoderCounters"; static final String DECODER_NAME = "decoderName"; static final String INITIALIZATION_DURATION_MS = "initializationDurationMs"; static final String FORMAT = "format"; static final String AUDIO_SESSION_ID = "audioSessionId"; static final String BUFFER_SIZE = "bufferSize"; static final String BUFFER_SIZE_MS = "bufferSizeMs"; static final String ELAPSED_SINCE_LAST_FEED_MS = "elapsedSinceLastFeedMs"; static final String DROPPED_FRAMES = "droppedFrames"; static final String ELAPSED_MS = "elapsedMs"; static final String PIXEL_WIDTH_HEIGHT_RATIO = "pixelWidthHeightRatio"; static final String UNAPPLIED_ROTATION_DEGREES = "unappliedRotationDegrees"; static final String SURFACE = "surface"; static final String TIMELINE = "timeline"; static final String MANIFEST = "manifest"; static final String WINDOW_INDEX = "windowIndex"; static final String MEDIA_PERIOD_ID = "mediaPeriodId"; } static final class Methods { private Methods() { // non-instantiable } static final String ON_PLAYER_STATE_CHANGED = "onPlayerStateChanged"; static final String ON_TIMELINE_CHANGED = "onTimelineChanged"; static final String ON_POSITION_DISCONTINUITY = "onPositionDiscontinuity"; static final String ON_SEEK_STARTED = "onSeekStarted"; static final String ON_SEEK_PROCESSED = "onSeekProcessed"; static final String ON_PLAYBACK_PARAMETERS_CHANGED = "onPlaybackParametersChanged"; static final String ON_REPEAT_MODE_CHANGED = "onRepeatModeChanged"; static final String ON_SHUFFLE_MODE_CHANGED = "onShuffleModeChanged"; static final String ON_PLAYER_ERROR = "onPlayerError"; static final String ON_TRACKS_CHANGED = "onTracksChanged"; static final String ON_LOAD_STARTED = "onLoadStarted"; static final String ON_LOAD_COMPLETED = "onLoadCompleted"; static final String ON_LOAD_CANCELED = "onLoadCanceled"; static final String ON_LOAD_ERROR = "onLoadError"; static final String ON_DOWNSTREAM_FORMAT_CHANGED = "onDownstreamFormatChanged"; static final String ON_UPSTREAM_DISCARDED = "onUpstreamDiscarded"; static final String ON_MEDIA_PERIOD_CREATED = "onMediaPeriodCreated"; static final String ON_MEDIA_PERIOD_RELEASED = "onMediaPeriodReleased"; static final String ON_READING_STARTED = "onReadingStarted"; static final String ON_BANDWIDTH_ESTIMATE = "onBandwidthEstimate"; static final String ON_SURFACE_SIZE_CHANGED = "onSurfaceSizeChanged"; static final String ON_METADATA = "onMetadata"; static final String ON_DECODER_ENABLED = "onDecoderEnabled"; static final String ON_DECODER_INITIALIZED = "onDecoderInitialized"; static final String ON_DECODER_INPUT_FORMAT_CHANGED = "onDecoderInputFormatChanged"; static final String ON_DECODER_DISABLED = "onDecoderDisabled"; static final String ON_AUDIO_SESSION_ID = "onAudioSessionId"; static final String ON_AUDIO_UNDERRUN = "onAudioUnderrun"; static final String ON_DROPPED_VIDEO_FRAMES = "onDroppedVideoFrames"; static final String ON_VIDEO_SIZE_CHANGED = "onVideoSizeChanged"; static final String ON_RENDERED_FIRST_FRAME = "onRenderedFirstFrame"; static final String ON_DRM_KEYS_LOADED = "onDrmKeysLoaded"; static final String ON_DRM_SESSION_MANAGER_ERROR = "onDrmSessionManagerError"; static final String ON_DRM_KEYS_RESTORED = "onDrmKeysRestored"; static final String ON_DRM_KEYS_REMOVED = "onDrmKeysRemoved"; static final String ON_LOADING_CHANGED = "onLoadingChanged"; static final String ON_SHUFFLE_MODE_ENABLED_CHANGED = "onShuffleModeEnabledChanged"; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/MediaSourceEventForwarder.java ================================================ package com.novoda.noplayer.internal.exoplayer.forwarder; import android.support.annotation.Nullable; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.novoda.noplayer.NoPlayer; import java.io.IOException; import java.util.HashMap; import static com.novoda.noplayer.internal.exoplayer.forwarder.ForwarderInformation.Methods; import static com.novoda.noplayer.internal.exoplayer.forwarder.ForwarderInformation.Parameters; // This implements an interface method defined by ExoPlayer @SuppressWarnings({"PMD.UnusedImports", "checkstyle:ParameterNumber", "PMD.ExcessiveParameterList"}) class MediaSourceEventForwarder implements MediaSourceEventListener { private static final String NO_MEDIA_PERIOD_ID = null; private final NoPlayer.InfoListener infoListener; MediaSourceEventForwarder(NoPlayer.InfoListener infoListener) { this.infoListener = infoListener; } @Override public void onMediaPeriodCreated(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.WINDOW_INDEX, String.valueOf(windowIndex)); callingMethodParameters.put(Parameters.MEDIA_PERIOD_ID, mediaPeriodId.toString()); infoListener.onNewInfo(Methods.ON_MEDIA_PERIOD_CREATED, callingMethodParameters); } @Override public void onMediaPeriodReleased(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.WINDOW_INDEX, String.valueOf(windowIndex)); callingMethodParameters.put(Parameters.MEDIA_PERIOD_ID, mediaPeriodId.toString()); infoListener.onNewInfo(Methods.ON_MEDIA_PERIOD_RELEASED, callingMethodParameters); } @Override public void onLoadStarted(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.WINDOW_INDEX, String.valueOf(windowIndex)); callingMethodParameters.put(Parameters.MEDIA_PERIOD_ID, mediaPeriodId == null ? NO_MEDIA_PERIOD_ID : mediaPeriodId.toString()); callingMethodParameters.put(Parameters.LOAD_EVENT_INFO, loadEventInfo.toString()); callingMethodParameters.put(Parameters.MEDIA_LOAD_DATA, mediaLoadData.toString()); infoListener.onNewInfo(Methods.ON_LOAD_STARTED, callingMethodParameters); } @Override public void onLoadCompleted(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.WINDOW_INDEX, String.valueOf(windowIndex)); callingMethodParameters.put(Parameters.MEDIA_PERIOD_ID, mediaPeriodId == null ? NO_MEDIA_PERIOD_ID : mediaPeriodId.toString()); callingMethodParameters.put(Parameters.LOAD_EVENT_INFO, loadEventInfo.toString()); callingMethodParameters.put(Parameters.MEDIA_LOAD_DATA, mediaLoadData.toString()); infoListener.onNewInfo(Methods.ON_LOAD_COMPLETED, callingMethodParameters); } @Override public void onLoadCanceled(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.WINDOW_INDEX, String.valueOf(windowIndex)); callingMethodParameters.put(Parameters.MEDIA_PERIOD_ID, mediaPeriodId == null ? NO_MEDIA_PERIOD_ID : mediaPeriodId.toString()); callingMethodParameters.put(Parameters.LOAD_EVENT_INFO, loadEventInfo.toString()); callingMethodParameters.put(Parameters.MEDIA_LOAD_DATA, mediaLoadData.toString()); infoListener.onNewInfo(Methods.ON_LOAD_CANCELED, callingMethodParameters); } @Override public void onLoadError(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData, IOException error, boolean wasCanceled) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.WINDOW_INDEX, String.valueOf(windowIndex)); callingMethodParameters.put(Parameters.MEDIA_PERIOD_ID, mediaPeriodId == null ? NO_MEDIA_PERIOD_ID : mediaPeriodId.toString()); callingMethodParameters.put(Parameters.LOAD_EVENT_INFO, loadEventInfo.toString()); callingMethodParameters.put(Parameters.MEDIA_LOAD_DATA, mediaLoadData.toString()); infoListener.onNewInfo(Methods.ON_LOAD_CANCELED, callingMethodParameters); } @Override public void onReadingStarted(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.WINDOW_INDEX, String.valueOf(windowIndex)); callingMethodParameters.put(Parameters.MEDIA_PERIOD_ID, mediaPeriodId.toString()); infoListener.onNewInfo(Methods.ON_READING_STARTED, callingMethodParameters); } @Override public void onUpstreamDiscarded(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.WINDOW_INDEX, String.valueOf(windowIndex)); callingMethodParameters.put(Parameters.MEDIA_PERIOD_ID, mediaPeriodId.toString()); callingMethodParameters.put(Parameters.MEDIA_LOAD_DATA, mediaLoadData.toString()); infoListener.onNewInfo(Methods.ON_UPSTREAM_DISCARDED, callingMethodParameters); } @Override public void onDownstreamFormatChanged(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put(Parameters.WINDOW_INDEX, String.valueOf(windowIndex)); callingMethodParameters.put(Parameters.MEDIA_PERIOD_ID, mediaPeriodId == null ? NO_MEDIA_PERIOD_ID : mediaPeriodId.toString()); callingMethodParameters.put(Parameters.MEDIA_LOAD_DATA, mediaLoadData.toString()); infoListener.onNewInfo(Methods.ON_DOWNSTREAM_FORMAT_CHANGED, callingMethodParameters); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/NoPlayerAnalyticsListener.java ================================================ package com.novoda.noplayer.internal.exoplayer.forwarder; import android.view.Surface; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.novoda.noplayer.NoPlayer; import java.io.IOException; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; class NoPlayerAnalyticsListener implements AnalyticsListener { private final List listeners = new CopyOnWriteArrayList<>(); private final List droppedVideoFramesListeners = new CopyOnWriteArrayList<>(); public void add(AnalyticsListener listener) { listeners.add(listener); } public void add(NoPlayer.DroppedVideoFramesListener listener) { droppedVideoFramesListeners.add(listener); } @Override public void onPlayerStateChanged(EventTime eventTime, boolean playWhenReady, int playbackState) { for (AnalyticsListener listener : listeners) { listener.onPlayerStateChanged(eventTime, playWhenReady, playbackState); } } @Override public void onTimelineChanged(EventTime eventTime, int reason) { for (AnalyticsListener listener : listeners) { listener.onTimelineChanged(eventTime, reason); } } @Override public void onPositionDiscontinuity(EventTime eventTime, int reason) { for (AnalyticsListener listener : listeners) { listener.onPositionDiscontinuity(eventTime, reason); } } @Override public void onSeekStarted(EventTime eventTime) { for (AnalyticsListener listener : listeners) { listener.onSeekStarted(eventTime); } } @Override public void onSeekProcessed(EventTime eventTime) { for (AnalyticsListener listener : listeners) { listener.onSeekProcessed(eventTime); } } @Override public void onPlaybackParametersChanged(EventTime eventTime, PlaybackParameters playbackParameters) { for (AnalyticsListener listener : listeners) { listener.onPlaybackParametersChanged(eventTime, playbackParameters); } } @Override public void onRepeatModeChanged(EventTime eventTime, int repeatMode) { for (AnalyticsListener listener : listeners) { listener.onRepeatModeChanged(eventTime, repeatMode); } } @Override public void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) { for (AnalyticsListener listener : listeners) { listener.onShuffleModeChanged(eventTime, shuffleModeEnabled); } } @Override public void onLoadingChanged(EventTime eventTime, boolean isLoading) { for (AnalyticsListener listener : listeners) { listener.onLoadingChanged(eventTime, isLoading); } } @Override public void onPlayerError(EventTime eventTime, ExoPlaybackException error) { for (AnalyticsListener listener : listeners) { listener.onPlayerError(eventTime, error); } } @Override public void onTracksChanged(EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { for (AnalyticsListener listener : listeners) { listener.onTracksChanged(eventTime, trackGroups, trackSelections); } } @Override public void onLoadStarted(EventTime eventTime, MediaSourceEventListener.LoadEventInfo loadEventInfo, MediaSourceEventListener.MediaLoadData mediaLoadData) { for (AnalyticsListener listener : listeners) { listener.onLoadStarted(eventTime, loadEventInfo, mediaLoadData); } } @Override public void onLoadCompleted(EventTime eventTime, MediaSourceEventListener.LoadEventInfo loadEventInfo, MediaSourceEventListener.MediaLoadData mediaLoadData) { for (AnalyticsListener listener : listeners) { listener.onLoadCompleted(eventTime, loadEventInfo, mediaLoadData); } } @Override public void onLoadCanceled(EventTime eventTime, MediaSourceEventListener.LoadEventInfo loadEventInfo, MediaSourceEventListener.MediaLoadData mediaLoadData) { for (AnalyticsListener listener : listeners) { listener.onLoadCanceled(eventTime, loadEventInfo, mediaLoadData); } } @Override public void onLoadError(EventTime eventTime, MediaSourceEventListener.LoadEventInfo loadEventInfo, MediaSourceEventListener.MediaLoadData mediaLoadData, IOException error, boolean wasCanceled) { for (AnalyticsListener listener : listeners) { listener.onLoadError(eventTime, loadEventInfo, mediaLoadData, error, wasCanceled); } } @Override public void onDownstreamFormatChanged(EventTime eventTime, MediaSourceEventListener.MediaLoadData mediaLoadData) { for (AnalyticsListener listener : listeners) { listener.onDownstreamFormatChanged(eventTime, mediaLoadData); } } @Override public void onUpstreamDiscarded(EventTime eventTime, MediaSourceEventListener.MediaLoadData mediaLoadData) { for (AnalyticsListener listener : listeners) { listener.onUpstreamDiscarded(eventTime, mediaLoadData); } } @Override public void onMediaPeriodCreated(EventTime eventTime) { for (AnalyticsListener listener : listeners) { listener.onMediaPeriodCreated(eventTime); } } @Override public void onMediaPeriodReleased(EventTime eventTime) { for (AnalyticsListener listener : listeners) { listener.onMediaPeriodReleased(eventTime); } } @Override public void onReadingStarted(EventTime eventTime) { for (AnalyticsListener listener : listeners) { listener.onReadingStarted(eventTime); } } @Override public void onBandwidthEstimate(EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) { for (AnalyticsListener listener : listeners) { listener.onBandwidthEstimate(eventTime, totalLoadTimeMs, totalBytesLoaded, bitrateEstimate); } } @Override public void onSurfaceSizeChanged(EventTime eventTime, int width, int height) { for (AnalyticsListener listener : listeners) { listener.onSurfaceSizeChanged(eventTime, width, height); } } @Override public void onMetadata(EventTime eventTime, Metadata metadata) { for (AnalyticsListener listener : listeners) { listener.onMetadata(eventTime, metadata); } } @Override public void onDecoderEnabled(EventTime eventTime, int trackType, DecoderCounters decoderCounters) { for (AnalyticsListener listener : listeners) { listener.onDecoderEnabled(eventTime, trackType, decoderCounters); } } @Override public void onDecoderInitialized(EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) { for (AnalyticsListener listener : listeners) { listener.onDecoderInitialized(eventTime, trackType, decoderName, initializationDurationMs); } } @Override public void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) { for (AnalyticsListener listener : listeners) { listener.onDecoderInputFormatChanged(eventTime, trackType, format); } } @Override public void onDecoderDisabled(EventTime eventTime, int trackType, DecoderCounters decoderCounters) { for (AnalyticsListener listener : listeners) { listener.onDecoderDisabled(eventTime, trackType, decoderCounters); } } @Override public void onAudioSessionId(EventTime eventTime, int audioSessionId) { for (AnalyticsListener listener : listeners) { listener.onAudioSessionId(eventTime, audioSessionId); } } @Override public void onAudioUnderrun(EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { for (AnalyticsListener listener : listeners) { listener.onAudioUnderrun(eventTime, bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); } } @Override public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) { for (AnalyticsListener listener : listeners) { listener.onDroppedVideoFrames(eventTime, droppedFrames, elapsedMs); } for (NoPlayer.DroppedVideoFramesListener listener : droppedVideoFramesListeners) { listener.onDroppedVideoFrames(droppedFrames, elapsedMs); } } @Override public void onVideoSizeChanged(EventTime eventTime, int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { for (AnalyticsListener listener : listeners) { listener.onVideoSizeChanged(eventTime, width, height, unappliedRotationDegrees, pixelWidthHeightRatio); } } @Override public void onRenderedFirstFrame(EventTime eventTime, Surface surface) { for (AnalyticsListener listener : listeners) { listener.onRenderedFirstFrame(eventTime, surface); } } @Override public void onDrmKeysLoaded(EventTime eventTime) { for (AnalyticsListener listener : listeners) { listener.onDrmKeysLoaded(eventTime); } } @Override public void onDrmSessionManagerError(EventTime eventTime, Exception error) { for (AnalyticsListener listener : listeners) { listener.onDrmSessionManagerError(eventTime, error); } } @Override public void onDrmKeysRestored(EventTime eventTime) { for (AnalyticsListener listener : listeners) { listener.onDrmKeysRestored(eventTime); } } @Override public void onDrmKeysRemoved(EventTime eventTime) { for (AnalyticsListener listener : listeners) { listener.onDrmKeysRemoved(eventTime); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/NoPlayerMediaSourceEventListener.java ================================================ package com.novoda.noplayer.internal.exoplayer.forwarder; import android.support.annotation.Nullable; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceEventListener; import java.io.IOException; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @SuppressWarnings({"checkstyle:ParameterNumber", "PMD.ExcessiveParameterList"}) // This implements an interface method defined by ExoPlayer class NoPlayerMediaSourceEventListener implements MediaSourceEventListener { private final List listeners = new CopyOnWriteArrayList<>(); public void add(MediaSourceEventListener listener) { listeners.add(listener); } @Override public void onMediaPeriodCreated(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { for (MediaSourceEventListener listener : listeners) { listener.onMediaPeriodCreated(windowIndex, mediaPeriodId); } } @Override public void onMediaPeriodReleased(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { for (MediaSourceEventListener listener : listeners) { listener.onMediaPeriodReleased(windowIndex, mediaPeriodId); } } @Override public void onLoadStarted(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { for (MediaSourceEventListener listener : listeners) { listener.onLoadStarted(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData); } } @Override public void onLoadCompleted(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { for (MediaSourceEventListener listener : listeners) { listener.onLoadCompleted(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData); } } @Override public void onLoadCanceled(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { for (MediaSourceEventListener listener : listeners) { listener.onLoadCanceled(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData); } } @Override public void onLoadError(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData, IOException error, boolean wasCanceled) { for (MediaSourceEventListener listener : listeners) { listener.onLoadError(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData, error, wasCanceled); } } @Override public void onReadingStarted(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { for (MediaSourceEventListener listener : listeners) { listener.onReadingStarted(windowIndex, mediaPeriodId); } } @Override public void onUpstreamDiscarded(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { for (MediaSourceEventListener listener : listeners) { listener.onUpstreamDiscarded(windowIndex, mediaPeriodId, mediaLoadData); } } @Override public void onDownstreamFormatChanged(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { for (MediaSourceEventListener listener : listeners) { listener.onDownstreamFormatChanged(windowIndex, mediaPeriodId, mediaLoadData); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/OnCompletionForwarder.java ================================================ package com.novoda.noplayer.internal.exoplayer.forwarder; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.novoda.noplayer.NoPlayer; class OnCompletionForwarder implements Player.EventListener { private final NoPlayer.CompletionListener completionListener; OnCompletionForwarder(NoPlayer.CompletionListener completionListener) { this.completionListener = completionListener; } @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { if (playbackState == Player.STATE_ENDED) { completionListener.onCompletion(); } } @Override public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { // TODO: should we send? } @Override public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { // TODO: should we send? } @Override public void onTimelineChanged(Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { // TODO: should we send? } @Override public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { // TODO: should we send? } @Override public void onLoadingChanged(boolean isLoading) { // TODO: should we send? } @Override public void onPlayerError(ExoPlaybackException error) { // Sent by ErrorForwarder. } @Override public void onPositionDiscontinuity(int reason) { // TODO: should we send? } @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { // TODO: should we send? } @Override public void onSeekProcessed() { // TODO: should we send? } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/OnCompletionStateChangedForwarder.java ================================================ package com.novoda.noplayer.internal.exoplayer.forwarder; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.novoda.noplayer.NoPlayer; class OnCompletionStateChangedForwarder implements Player.EventListener { private final NoPlayer.StateChangedListener stateChangedListener; OnCompletionStateChangedForwarder(NoPlayer.StateChangedListener stateChangedListener) { this.stateChangedListener = stateChangedListener; } @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { if (playbackState == Player.STATE_ENDED) { stateChangedListener.onVideoStopped(); } } @Override public void onRepeatModeChanged(int i) { // TODO: should we send? } @Override public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { // TODO: should we send? } @Override public void onTimelineChanged(Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { // TODO: should we send? } @Override public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { // TODO: should we send? } @Override public void onLoadingChanged(boolean isLoading) { // TODO: should we send? } @Override public void onPlayerError(ExoPlaybackException error) { // Sent by ErrorForwarder. } @Override public void onPositionDiscontinuity(int reason) { // TODO: should we send? } @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { // TODO: should we send? } @Override public void onSeekProcessed() { // TODO: should we send? } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/OnPrepareForwarder.java ================================================ package com.novoda.noplayer.internal.exoplayer.forwarder; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.PlayerState; class OnPrepareForwarder implements Player.EventListener { private final NoPlayer.PreparedListener preparedListener; private final PlayerState playerState; OnPrepareForwarder(NoPlayer.PreparedListener preparedListener, PlayerState playerState) { this.preparedListener = preparedListener; this.playerState = playerState; } @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { if (isReady(playbackState)) { preparedListener.onPrepared(playerState); } } @Override public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { // TODO: should we send? } @Override public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { // TODO: should we send? } private boolean isReady(int playbackState) { return playbackState == Player.STATE_READY; } @Override public void onTimelineChanged(Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { // TODO: should we send? } @Override public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { // TODO: should we send? } @Override public void onLoadingChanged(boolean isLoading) { // TODO: should we send? } @Override public void onPlayerError(ExoPlaybackException error) { // Sent by ErrorForwarder. } @Override public void onPositionDiscontinuity(int reason) { // TODO: should we send? } @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { // TODO: should we send? } @Override public void onSeekProcessed() { // TODO: should we send? } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/PlayerOnErrorForwarder.java ================================================ package com.novoda.noplayer.internal.exoplayer.forwarder; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.internal.exoplayer.error.ExoPlayerErrorMapper; class PlayerOnErrorForwarder implements Player.EventListener { private final NoPlayer.ErrorListener errorListener; PlayerOnErrorForwarder(NoPlayer.ErrorListener errorListener) { this.errorListener = errorListener; } @Override public void onPlayerError(ExoPlaybackException error) { NoPlayer.PlayerError playerError = ExoPlayerErrorMapper.errorFor(error); errorListener.onError(playerError); } @Override public void onPositionDiscontinuity(int reason) { // TODO: should we send? } @Override public void onTimelineChanged(Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { // TODO: should we send? } @Override public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { // TODO: should we send? } @Override public void onLoadingChanged(boolean isLoading) { // TODO: should we send? } @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { // Handled by OnPrepared and OnCompletion forwarders. } @Override public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { // TODO: should we send? } @Override public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { // TODO: should we send? } @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { // TODO: should we send? } @Override public void onSeekProcessed() { // TODO: should we send? } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/VideoSizeChangedForwarder.java ================================================ package com.novoda.noplayer.internal.exoplayer.forwarder; import com.google.android.exoplayer2.video.VideoListener; import com.novoda.noplayer.NoPlayer; class VideoSizeChangedForwarder implements VideoListener { private final NoPlayer.VideoSizeChangedListener videoSizeChangedListener; VideoSizeChangedForwarder(NoPlayer.VideoSizeChangedListener videoSizeChangedListener) { this.videoSizeChangedListener = videoSizeChangedListener; } @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { videoSizeChangedListener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); } @Override public void onRenderedFirstFrame() { // TODO: should we send? } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/mediasource/AudioTrackType.java ================================================ package com.novoda.noplayer.internal.exoplayer.mediasource; public enum AudioTrackType { MAIN(1), ALTERNATIVE(0), UNKNOWN(-1); private final int selectionFlag; AudioTrackType(int selectionFlag) { this.selectionFlag = selectionFlag; } static AudioTrackType from(int selectionFlag) { for (AudioTrackType audioTrackType : AudioTrackType.values()) { if (audioTrackType.selectionFlag == selectionFlag) { return audioTrackType; } } return UNKNOWN; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/mediasource/ExoPlayerAudioTrackSelector.java ================================================ package com.novoda.noplayer.internal.exoplayer.mediasource; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.novoda.noplayer.internal.exoplayer.RendererTypeRequester; import com.novoda.noplayer.model.AudioTracks; import com.novoda.noplayer.model.PlayerAudioTrack; import java.util.ArrayList; import java.util.List; import static com.novoda.noplayer.internal.exoplayer.mediasource.TrackType.AUDIO; public class ExoPlayerAudioTrackSelector { private final ExoPlayerTrackSelector trackSelector; public ExoPlayerAudioTrackSelector(ExoPlayerTrackSelector trackSelector) { this.trackSelector = trackSelector; } public boolean selectAudioTrack(PlayerAudioTrack audioTrack, RendererTypeRequester rendererTypeRequester) { TrackGroupArray trackGroups = trackSelector.trackGroups(AUDIO, rendererTypeRequester); DefaultTrackSelector.SelectionOverride selectionOverride = new DefaultTrackSelector.SelectionOverride( audioTrack.groupIndex(), audioTrack.formatIndex() ); return trackSelector.setSelectionOverride(AUDIO, rendererTypeRequester, trackGroups, selectionOverride); } public AudioTracks getAudioTracks(RendererTypeRequester rendererTypeRequester) { TrackGroupArray trackGroups = trackSelector.trackGroups(AUDIO, rendererTypeRequester); List audioTracks = new ArrayList<>(); for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) { if (trackSelector.supportsTrackSwitching(AUDIO, rendererTypeRequester, trackGroups, groupIndex)) { TrackGroup trackGroup = trackGroups.get(groupIndex); for (int formatIndex = 0; formatIndex < trackGroup.length; formatIndex++) { Format format = trackGroup.getFormat(formatIndex); PlayerAudioTrack playerAudioTrack = new PlayerAudioTrack( groupIndex, formatIndex, format.id, format.language, format.sampleMimeType, format.channelCount, format.bitrate, AudioTrackType.from(format.selectionFlags) ); audioTracks.add(playerAudioTrack); } } } return AudioTracks.from(audioTracks); } public boolean clearAudioTrack(RendererTypeRequester rendererTypeRequester) { return trackSelector.clearSelectionOverrideFor(AUDIO, rendererTypeRequester); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/mediasource/ExoPlayerMappedTrackInfo.java ================================================ package com.novoda.noplayer.internal.exoplayer.mediasource; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; class ExoPlayerMappedTrackInfo { private final MappingTrackSelector.MappedTrackInfo mappedTrackInfo; ExoPlayerMappedTrackInfo(MappingTrackSelector.MappedTrackInfo mappedTrackInfo) { this.mappedTrackInfo = mappedTrackInfo; } TrackGroupArray getTrackGroups(int index) { return mappedTrackInfo.getTrackGroups(index); } int getAdaptiveSupport(int rendererIndex, int groupIndex, boolean includeCapabilitiesExceededTracks) { return mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, includeCapabilitiesExceededTracks); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/mediasource/ExoPlayerSubtitleTrackSelector.java ================================================ package com.novoda.noplayer.internal.exoplayer.mediasource; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.novoda.noplayer.internal.exoplayer.RendererTypeRequester; import com.novoda.noplayer.model.PlayerSubtitleTrack; import java.util.ArrayList; import java.util.List; import static com.novoda.noplayer.internal.exoplayer.mediasource.TrackType.TEXT; public class ExoPlayerSubtitleTrackSelector { private final ExoPlayerTrackSelector trackSelector; public ExoPlayerSubtitleTrackSelector(ExoPlayerTrackSelector trackSelector) { this.trackSelector = trackSelector; } public boolean selectTextTrack(PlayerSubtitleTrack subtitleTrack, RendererTypeRequester rendererTypeRequester) { TrackGroupArray trackGroups = trackSelector.trackGroups(TEXT, rendererTypeRequester); DefaultTrackSelector.SelectionOverride selectionOverride = new DefaultTrackSelector.SelectionOverride( subtitleTrack.groupIndex(), subtitleTrack.formatIndex() ); return trackSelector.setSelectionOverride(TEXT, rendererTypeRequester, trackGroups, selectionOverride); } public List getSubtitleTracks(RendererTypeRequester rendererTypeRequester) { TrackGroupArray trackGroups = trackSelector.trackGroups(TEXT, rendererTypeRequester); List subtitleTracks = new ArrayList<>(); for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) { TrackGroup trackGroup = trackGroups.get(groupIndex); for (int formatIndex = 0; formatIndex < trackGroup.length; formatIndex++) { Format format = trackGroup.getFormat(formatIndex); PlayerSubtitleTrack playerSubtitleTrack = new PlayerSubtitleTrack( groupIndex, formatIndex, format.id, format.language, format.sampleMimeType, format.channelCount, format.bitrate ); subtitleTracks.add(playerSubtitleTrack); } } return subtitleTracks; } public boolean clearSubtitleTrack(RendererTypeRequester rendererTypeRequester) { return trackSelector.clearSelectionOverrideFor(TEXT, rendererTypeRequester); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/mediasource/ExoPlayerTrackSelector.java ================================================ package com.novoda.noplayer.internal.exoplayer.mediasource; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.novoda.noplayer.internal.exoplayer.RendererTypeRequester; import com.novoda.noplayer.internal.utils.Optional; // We cannot make it final as we need to mock it in tests @SuppressWarnings({"checkstyle:FinalClass", "PMD.ClassWithOnlyPrivateConstructorsShouldBeFinal"}) public class ExoPlayerTrackSelector { private final DefaultTrackSelector trackSelector; private final RendererTrackIndexExtractor rendererTrackIndexExtractor; public static ExoPlayerTrackSelector newInstance(DefaultTrackSelector trackSelector) { RendererTrackIndexExtractor rendererTrackIndexExtractor = new RendererTrackIndexExtractor(); return new ExoPlayerTrackSelector(trackSelector, rendererTrackIndexExtractor); } private ExoPlayerTrackSelector(DefaultTrackSelector trackSelector, RendererTrackIndexExtractor rendererTrackIndexExtractor) { this.trackSelector = trackSelector; this.rendererTrackIndexExtractor = rendererTrackIndexExtractor; } TrackGroupArray trackGroups(TrackType trackType, RendererTypeRequester rendererTypeRequester) { Optional audioRendererIndex = rendererTrackIndexExtractor.extract(trackType, mappedTrackInfoLength(), rendererTypeRequester); return audioRendererIndex.isAbsent() ? TrackGroupArray.EMPTY : trackInfo().getTrackGroups(audioRendererIndex.get()); } boolean clearSelectionOverrideFor(TrackType trackType, RendererTypeRequester rendererTypeRequester) { Optional rendererIndex = rendererTrackIndexExtractor.extract(trackType, mappedTrackInfoLength(), rendererTypeRequester); if (rendererIndex.isPresent()) { trackSelector.setParameters(trackSelector .buildUponParameters() .clearSelectionOverrides(rendererIndex.get()) ); return true; } else { return false; } } private ExoPlayerMappedTrackInfo trackInfo() { MappingTrackSelector.MappedTrackInfo trackInfo = trackSelector.getCurrentMappedTrackInfo(); if (trackInfo == null) { throw new IllegalStateException("Track info is not available."); } return new ExoPlayerMappedTrackInfo(trackInfo); } private int mappedTrackInfoLength() { return trackSelector.getCurrentMappedTrackInfo().length; } boolean setSelectionOverride(TrackType trackType, RendererTypeRequester rendererTypeRequester, TrackGroupArray trackGroups, DefaultTrackSelector.SelectionOverride selectionOverride) { Optional rendererIndex = rendererTrackIndexExtractor.extract(trackType, mappedTrackInfoLength(), rendererTypeRequester); if (rendererIndex.isPresent()) { trackSelector.setParameters(trackSelector .buildUponParameters() .setSelectionOverride(rendererIndex.get(), trackGroups, selectionOverride) ); return true; } else { return false; } } boolean supportsTrackSwitching(TrackType trackType, RendererTypeRequester rendererTypeRequester, TrackGroupArray trackGroups, int groupIndex) { Optional audioRendererIndex = rendererTrackIndexExtractor.extract(trackType, mappedTrackInfoLength(), rendererTypeRequester); return audioRendererIndex.isPresent() && trackGroups.get(groupIndex).length > 0 && trackInfo().getAdaptiveSupport(audioRendererIndex.get(), groupIndex, false) != RendererCapabilities.ADAPTIVE_NOT_SUPPORTED; } void clearMaxVideoBitrate() { setMaxVideoBitrateParameter(Integer.MAX_VALUE); } void setMaxVideoBitrate(int maxVideoBitrate) { setMaxVideoBitrateParameter(maxVideoBitrate); } private void setMaxVideoBitrateParameter(int maxValue) { trackSelector.setParameters( trackSelector.buildUponParameters() .setMaxVideoBitrate(maxValue) .build() ); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/mediasource/ExoPlayerVideoTrackSelector.java ================================================ package com.novoda.noplayer.internal.exoplayer.mediasource; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.novoda.noplayer.ContentType; import com.novoda.noplayer.internal.exoplayer.RendererTypeRequester; import com.novoda.noplayer.internal.utils.Optional; import com.novoda.noplayer.model.PlayerVideoTrack; import java.util.ArrayList; import java.util.List; import static com.novoda.noplayer.internal.exoplayer.mediasource.TrackType.VIDEO; public class ExoPlayerVideoTrackSelector { private final ExoPlayerTrackSelector trackSelector; public ExoPlayerVideoTrackSelector(ExoPlayerTrackSelector trackSelector) { this.trackSelector = trackSelector; } public boolean selectVideoTrack(PlayerVideoTrack videoTrack, RendererTypeRequester rendererTypeRequester) { TrackGroupArray trackGroups = trackSelector.trackGroups(VIDEO, rendererTypeRequester); DefaultTrackSelector.SelectionOverride selectionOverride = new DefaultTrackSelector.SelectionOverride( videoTrack.groupIndex(), videoTrack.formatIndex() ); return trackSelector.setSelectionOverride(VIDEO, rendererTypeRequester, trackGroups, selectionOverride); } public List getVideoTracks(RendererTypeRequester rendererTypeRequester, ContentType contentType) { TrackGroupArray trackGroups = trackSelector.trackGroups(VIDEO, rendererTypeRequester); List videoTracks = new ArrayList<>(); for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) { TrackGroup trackGroup = trackGroups.get(groupIndex); for (int formatIndex = 0; formatIndex < trackGroup.length; formatIndex++) { Format format = trackGroup.getFormat(formatIndex); PlayerVideoTrack playerVideoTrack = new PlayerVideoTrack( groupIndex, formatIndex, format.id, contentType, format.width, format.height, (int) format.frameRate, format.bitrate ); videoTracks.add(playerVideoTrack); } } return videoTracks; } public Optional getSelectedVideoTrack(SimpleExoPlayer exoPlayer, RendererTypeRequester rendererTypeRequester, ContentType contentType) { Format selectedVideoFormat = exoPlayer.getVideoFormat(); if (selectedVideoFormat == null) { return Optional.absent(); } List videoTracks = getVideoTracks(rendererTypeRequester, contentType); return findSelectedVideoTrack(selectedVideoFormat, videoTracks); } private Optional findSelectedVideoTrack(Format selectedVideoFormat, List videoTracks) { for (PlayerVideoTrack videoTrack : videoTracks) { if (videoTrack.id().equals(selectedVideoFormat.id)) { return Optional.of(videoTrack); } } return Optional.absent(); } public boolean clearVideoTrack(RendererTypeRequester rendererTypeRequester) { return trackSelector.clearSelectionOverrideFor(VIDEO, rendererTypeRequester); } public void clearMaxVideoBitrate() { trackSelector.clearMaxVideoBitrate(); } public void setMaxVideoBitrate(int maxVideoBitrate) { trackSelector.setMaxVideoBitrate(maxVideoBitrate); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/mediasource/MediaSourceFactory.java ================================================ package com.novoda.noplayer.internal.exoplayer.mediasource; import android.content.Context; import android.net.Uri; import android.os.Handler; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.novoda.noplayer.Options; import com.novoda.noplayer.internal.utils.Optional; public class MediaSourceFactory { private final Context context; private final Handler handler; private final Optional dataSourceFactory; private final String userAgent; private final boolean allowCrossProtocolRedirects; public MediaSourceFactory(Context context, String userAgent, Handler handler, Optional dataSourceFactory, boolean allowCrossProtocolRedirects) { this.context = context; this.handler = handler; this.dataSourceFactory = dataSourceFactory; this.userAgent = userAgent; this.allowCrossProtocolRedirects = allowCrossProtocolRedirects; } public MediaSource create(Options options, Uri uri, MediaSourceEventListener mediaSourceEventListener, DefaultBandwidthMeter bandwidthMeter) { DefaultDataSourceFactory defaultDataSourceFactory = createDataSourceFactory(bandwidthMeter); switch (options.contentType()) { case HLS: return createHlsMediaSource(defaultDataSourceFactory, uri, mediaSourceEventListener); case H264: return createH264MediaSource(defaultDataSourceFactory, uri, mediaSourceEventListener); case DASH: return createDashMediaSource(defaultDataSourceFactory, uri, mediaSourceEventListener); default: throw new UnsupportedOperationException("Content type: " + options + " is not supported."); } } private DefaultDataSourceFactory createDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) { if (dataSourceFactory.isPresent()) { return new DefaultDataSourceFactory(context, bandwidthMeter, dataSourceFactory.get()); } else { DefaultHttpDataSourceFactory httpDataSourceFactory = new DefaultHttpDataSourceFactory( userAgent, bandwidthMeter, DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, allowCrossProtocolRedirects ); return new DefaultDataSourceFactory(context, bandwidthMeter, httpDataSourceFactory); } } private MediaSource createHlsMediaSource(DefaultDataSourceFactory defaultDataSourceFactory, Uri uri, MediaSourceEventListener mediaSourceEventListener) { HlsMediaSource.Factory factory = new HlsMediaSource.Factory(defaultDataSourceFactory); HlsMediaSource hlsMediaSource = factory.createMediaSource(uri); hlsMediaSource.addEventListener(handler, mediaSourceEventListener); return hlsMediaSource; } private MediaSource createH264MediaSource(DefaultDataSourceFactory defaultDataSourceFactory, Uri uri, MediaSourceEventListener mediaSourceEventListener) { ExtractorMediaSource.Factory factory = new ExtractorMediaSource.Factory(defaultDataSourceFactory); ExtractorMediaSource extractorMediaSource = factory .setExtractorsFactory(new DefaultExtractorsFactory()) .createMediaSource(uri); extractorMediaSource.addEventListener(handler, mediaSourceEventListener); return extractorMediaSource; } private MediaSource createDashMediaSource(DefaultDataSourceFactory defaultDataSourceFactory, Uri uri, MediaSourceEventListener mediaSourceEventListener) { DefaultDashChunkSource.Factory chunkSourceFactory = new DefaultDashChunkSource.Factory(defaultDataSourceFactory); DashMediaSource.Factory factory = new DashMediaSource.Factory(chunkSourceFactory, defaultDataSourceFactory); DashMediaSource mediaSource = factory.createMediaSource(uri); mediaSource.addEventListener(handler, mediaSourceEventListener); return mediaSource; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/mediasource/RendererTrackIndexExtractor.java ================================================ package com.novoda.noplayer.internal.exoplayer.mediasource; import com.google.android.exoplayer2.C; import com.novoda.noplayer.internal.exoplayer.RendererTypeRequester; import com.novoda.noplayer.internal.utils.Optional; class RendererTrackIndexExtractor { Optional extract(TrackType trackType, int numberOfTracks, RendererTypeRequester typeRequester) { for (int i = 0; i < numberOfTracks; i++) { int rendererType = typeRequester.getRendererTypeFor(i); if ((trackType == TrackType.AUDIO && rendererType == C.TRACK_TYPE_AUDIO) || (trackType == TrackType.VIDEO && rendererType == C.TRACK_TYPE_VIDEO) || (trackType == TrackType.TEXT && rendererType == C.TRACK_TYPE_TEXT)) { return Optional.of(i); } } return Optional.absent(); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/exoplayer/mediasource/TrackType.java ================================================ package com.novoda.noplayer.internal.exoplayer.mediasource; enum TrackType { AUDIO, VIDEO, TEXT } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/listeners/BitrateChangedListeners.java ================================================ package com.novoda.noplayer.internal.listeners; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.model.Bitrate; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; class BitrateChangedListeners implements NoPlayer.BitrateChangedListener { private final Set listeners = new CopyOnWriteArraySet<>(); void add(NoPlayer.BitrateChangedListener listener) { listeners.add(listener); } void remove(NoPlayer.BitrateChangedListener listener) { listeners.remove(listener); } void clear() { listeners.clear(); } @Override public void onBitrateChanged(Bitrate audioBitrate, Bitrate videoBitrate) { for (NoPlayer.BitrateChangedListener listener : listeners) { listener.onBitrateChanged(audioBitrate, videoBitrate); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/listeners/BufferStateListeners.java ================================================ package com.novoda.noplayer.internal.listeners; import com.novoda.noplayer.NoPlayer; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; class BufferStateListeners implements NoPlayer.BufferStateListener { private final Set listeners = new CopyOnWriteArraySet<>(); void add(NoPlayer.BufferStateListener listener) { listeners.add(listener); } void remove(NoPlayer.BufferStateListener listener) { listeners.remove(listener); } void clear() { listeners.clear(); } @Override public void onBufferStarted() { for (NoPlayer.BufferStateListener listener : listeners) { listener.onBufferStarted(); } } @Override public void onBufferCompleted() { for (NoPlayer.BufferStateListener listener : listeners) { listener.onBufferCompleted(); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/listeners/CompletionListeners.java ================================================ package com.novoda.noplayer.internal.listeners; import com.novoda.noplayer.NoPlayer; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; class CompletionListeners implements NoPlayer.CompletionListener { private final Set listeners = new CopyOnWriteArraySet<>(); private boolean hasCompleted; void add(NoPlayer.CompletionListener listener) { listeners.add(listener); } void remove(NoPlayer.CompletionListener listener) { listeners.remove(listener); } void clear() { listeners.clear(); } public void onCompletion() { if (!hasCompleted) { hasCompleted = true; for (NoPlayer.CompletionListener listener : listeners) { listener.onCompletion(); } } } void resetCompletedState() { hasCompleted = false; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/listeners/DroppedFramesListeners.java ================================================ package com.novoda.noplayer.internal.listeners; import com.novoda.noplayer.NoPlayer; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; public class DroppedFramesListeners implements NoPlayer.DroppedVideoFramesListener { private final Set listeners = new CopyOnWriteArraySet<>(); void add(NoPlayer.DroppedVideoFramesListener listener) { listeners.add(listener); } void remove(NoPlayer.DroppedVideoFramesListener listener) { listeners.remove(listener); } void clear() { listeners.clear(); } @Override public void onDroppedVideoFrames(int droppedFrames, long elapsedMsSinceLastDroppedFrames) { for (NoPlayer.DroppedVideoFramesListener listener : listeners) { listener.onDroppedVideoFrames(droppedFrames, elapsedMsSinceLastDroppedFrames); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/listeners/ErrorListeners.java ================================================ package com.novoda.noplayer.internal.listeners; import com.novoda.noplayer.NoPlayer; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; class ErrorListeners implements NoPlayer.ErrorListener { private final Set listeners = new CopyOnWriteArraySet<>(); void add(NoPlayer.ErrorListener listener) { listeners.add(listener); } void remove(NoPlayer.ErrorListener listener) { listeners.remove(listener); } void clear() { listeners.clear(); } @Override public void onError(NoPlayer.PlayerError error) { for (NoPlayer.ErrorListener listener : listeners) { listener.onError(error); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/listeners/HeartbeatCallbacks.java ================================================ package com.novoda.noplayer.internal.listeners; import com.novoda.noplayer.NoPlayer; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; class HeartbeatCallbacks implements NoPlayer.HeartbeatCallback { private final Set callbacks = new CopyOnWriteArraySet<>(); void registerCallback(NoPlayer.HeartbeatCallback heartbeatCallback) { callbacks.add(heartbeatCallback); } void clear() { callbacks.clear(); } @Override public void onBeat(NoPlayer player) { for (NoPlayer.HeartbeatCallback callback : callbacks) { callback.onBeat(player); } } void unregisterCallback(NoPlayer.HeartbeatCallback heartbeatCallback) { callbacks.remove(heartbeatCallback); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/listeners/InfoListeners.java ================================================ package com.novoda.noplayer.internal.listeners; import com.novoda.noplayer.NoPlayer; import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; class InfoListeners implements NoPlayer.InfoListener { private final Set listeners = new CopyOnWriteArraySet<>(); void add(NoPlayer.InfoListener listener) { listeners.add(listener); } void remove(NoPlayer.InfoListener listener) { listeners.remove(listener); } void clear() { listeners.clear(); } @Override public void onNewInfo(String callingMethod, Map callingMethodParams) { for (NoPlayer.InfoListener listener : listeners) { listener.onNewInfo(callingMethod, callingMethodParams); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/listeners/PlayerListenersHolder.java ================================================ package com.novoda.noplayer.internal.listeners; import com.novoda.noplayer.Listeners; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.NoPlayer.BitrateChangedListener; public class PlayerListenersHolder implements Listeners { private final ErrorListeners errorListeners; private final PreparedListeners preparedListeners; private final BufferStateListeners bufferStateListeners; private final CompletionListeners completionListeners; private final StateChangedListeners stateChangedListeners; private final InfoListeners infoListeners; private final VideoSizeChangedListeners videoSizeChangedListeners; private final BitrateChangedListeners bitrateChangedListeners; private final DroppedFramesListeners droppedFramesListeners; private final HeartbeatCallbacks heartbeatCallbacks; public PlayerListenersHolder() { errorListeners = new ErrorListeners(); preparedListeners = new PreparedListeners(); bufferStateListeners = new BufferStateListeners(); completionListeners = new CompletionListeners(); stateChangedListeners = new StateChangedListeners(); infoListeners = new InfoListeners(); videoSizeChangedListeners = new VideoSizeChangedListeners(); bitrateChangedListeners = new BitrateChangedListeners(); heartbeatCallbacks = new HeartbeatCallbacks(); droppedFramesListeners = new DroppedFramesListeners(); } @Override public void addErrorListener(NoPlayer.ErrorListener errorListener) { errorListeners.add(errorListener); } @Override public void removeErrorListener(NoPlayer.ErrorListener errorListener) { errorListeners.remove(errorListener); } @Override public void addPreparedListener(NoPlayer.PreparedListener preparedListener) { preparedListeners.add(preparedListener); } @Override public void removePreparedListener(NoPlayer.PreparedListener preparedListener) { preparedListeners.remove(preparedListener); } @Override public void addBufferStateListener(NoPlayer.BufferStateListener bufferStateListener) { bufferStateListeners.add(bufferStateListener); } @Override public void removeBufferStateListener(NoPlayer.BufferStateListener bufferStateListener) { bufferStateListeners.remove(bufferStateListener); } @Override public void addCompletionListener(NoPlayer.CompletionListener completionListener) { completionListeners.add(completionListener); } @Override public void removeCompletionListener(NoPlayer.CompletionListener completionListener) { completionListeners.remove(completionListener); } @Override public void addStateChangedListener(NoPlayer.StateChangedListener stateChangedListener) { stateChangedListeners.add(stateChangedListener); } @Override public void removeStateChangedListener(NoPlayer.StateChangedListener stateChangedListener) { stateChangedListeners.remove(stateChangedListener); } @Override public void addInfoListener(NoPlayer.InfoListener infoListener) { infoListeners.add(infoListener); } @Override public void removeInfoListener(NoPlayer.InfoListener infoListener) { infoListeners.remove(infoListener); } @Override public void addBitrateChangedListener(BitrateChangedListener bitrateChangedListener) { bitrateChangedListeners.add(bitrateChangedListener); } @Override public void removeBitrateChangedListener(BitrateChangedListener bitrateChangedListener) { bitrateChangedListeners.remove(bitrateChangedListener); } @Override public void addHeartbeatCallback(NoPlayer.HeartbeatCallback heartbeatCallback) { heartbeatCallbacks.registerCallback(heartbeatCallback); } @Override public void removeHeartbeatCallback(NoPlayer.HeartbeatCallback heartbeatCallback) { heartbeatCallbacks.unregisterCallback(heartbeatCallback); } @Override public void addVideoSizeChangedListener(NoPlayer.VideoSizeChangedListener videoSizeChangedListener) { videoSizeChangedListeners.add(videoSizeChangedListener); } @Override public void removeVideoSizeChangedListener(NoPlayer.VideoSizeChangedListener videoSizeChangedListener) { videoSizeChangedListeners.remove(videoSizeChangedListener); } @Override public void addDroppedVideoFrames(NoPlayer.DroppedVideoFramesListener droppedVideoFramesListener) { droppedFramesListeners.add(droppedVideoFramesListener); } @Override public void removeDroppedVideoFrames(NoPlayer.DroppedVideoFramesListener droppedVideoFramesListener) { droppedFramesListeners.remove(droppedVideoFramesListener); } public NoPlayer.ErrorListener getErrorListeners() { return errorListeners; } public NoPlayer.PreparedListener getPreparedListeners() { return preparedListeners; } public NoPlayer.BufferStateListener getBufferStateListeners() { return bufferStateListeners; } public NoPlayer.CompletionListener getCompletionListeners() { return completionListeners; } public NoPlayer.StateChangedListener getStateChangedListeners() { return stateChangedListeners; } public NoPlayer.InfoListener getInfoListeners() { return infoListeners; } public NoPlayer.HeartbeatCallback getHeartbeatCallbacks() { return heartbeatCallbacks; } public NoPlayer.VideoSizeChangedListener getVideoSizeChangedListeners() { return videoSizeChangedListeners; } public NoPlayer.BitrateChangedListener getBitrateChangedListeners() { return bitrateChangedListeners; } public NoPlayer.DroppedVideoFramesListener getDroppedVideoFramesListeners() { return droppedFramesListeners; } public void resetState() { preparedListeners.resetPreparedState(); completionListeners.resetCompletedState(); } public void clear() { errorListeners.clear(); preparedListeners.clear(); bufferStateListeners.clear(); completionListeners.clear(); stateChangedListeners.clear(); infoListeners.clear(); videoSizeChangedListeners.clear(); bitrateChangedListeners.clear(); heartbeatCallbacks.clear(); droppedFramesListeners.clear(); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/listeners/PreparedListeners.java ================================================ package com.novoda.noplayer.internal.listeners; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.PlayerState; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; class PreparedListeners implements NoPlayer.PreparedListener { private final Set listeners = new CopyOnWriteArraySet<>(); private boolean hasPrepared; void add(NoPlayer.PreparedListener listener) { listeners.add(listener); } void remove(NoPlayer.PreparedListener listener) { listeners.remove(listener); } void clear() { listeners.clear(); } @Override public void onPrepared(PlayerState playerState) { if (!hasPrepared) { hasPrepared = true; for (NoPlayer.PreparedListener listener : listeners) { listener.onPrepared(playerState); } } } void resetPreparedState() { hasPrepared = false; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/listeners/StateChangedListeners.java ================================================ package com.novoda.noplayer.internal.listeners; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.internal.utils.NoPlayerLog; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; class StateChangedListeners implements NoPlayer.StateChangedListener { private enum State { PLAYING, PAUSED, STOPPED } private State currentState; private final Set listeners = new CopyOnWriteArraySet<>(); void add(NoPlayer.StateChangedListener listener) { listeners.add(listener); } void remove(NoPlayer.StateChangedListener listener) { listeners.remove(listener); } void clear() { listeners.clear(); } @Override public void onVideoPlaying() { if (currentState == State.PLAYING) { NoPlayerLog.e("Tried calling onVideoPlaying() but video is already playing."); return; } for (NoPlayer.StateChangedListener listener : listeners) { listener.onVideoPlaying(); } currentState = State.PLAYING; } @Override public void onVideoPaused() { if (currentState == State.PAUSED) { NoPlayerLog.e("Tried calling onVideoPaused() but video is already paused."); return; } for (NoPlayer.StateChangedListener listener : listeners) { listener.onVideoPaused(); } currentState = State.PAUSED; } @Override public void onVideoStopped() { if (currentState == State.STOPPED) { NoPlayerLog.e("Tried calling onVideoStopped() but video has already stopped."); return; } for (NoPlayer.StateChangedListener listener : listeners) { listener.onVideoStopped(); } currentState = State.STOPPED; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/listeners/VideoSizeChangedListeners.java ================================================ package com.novoda.noplayer.internal.listeners; import com.novoda.noplayer.NoPlayer; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; class VideoSizeChangedListeners implements NoPlayer.VideoSizeChangedListener { private final Set listeners = new CopyOnWriteArraySet<>(); void add(NoPlayer.VideoSizeChangedListener listener) { listeners.add(listener); } void remove(NoPlayer.VideoSizeChangedListener listener) { listeners.remove(listener); } void clear() { listeners.clear(); } @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { for (NoPlayer.VideoSizeChangedListener listener : listeners) { listener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/AndroidMediaPlayerAudioTrackSelector.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import android.media.MediaPlayer; import com.novoda.noplayer.internal.exoplayer.mediasource.AudioTrackType; import com.novoda.noplayer.model.AudioTracks; import com.novoda.noplayer.model.PlayerAudioTrack; import java.util.ArrayList; import java.util.List; class AndroidMediaPlayerAudioTrackSelector { private static final int NO_FORMAT = 0; private static final int NO_CHANNELS = -1; private static final int NO_FREQUENCY = -1; private static final String NO_MIME_TYPE = ""; private final TrackInfosFactory trackInfosFactory; AndroidMediaPlayerAudioTrackSelector(TrackInfosFactory trackInfosFactory) { this.trackInfosFactory = trackInfosFactory; } AudioTracks getAudioTracks(MediaPlayer mediaPlayer) { if (mediaPlayer == null) { throw new IllegalStateException("You can only call getAudioTracks() when video is prepared."); } List audioTracks = new ArrayList<>(); NoPlayerTrackInfos trackInfos = trackInfosFactory.createFrom(mediaPlayer); for (int i = 0; i < trackInfos.size(); i++) { NoPlayerTrackInfo trackInfo = trackInfos.get(i); if (trackInfo.type() == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO) { audioTracks.add( new PlayerAudioTrack( i, NO_FORMAT, String.valueOf(trackInfo.hashCode()), trackInfo.language(), NO_MIME_TYPE, NO_CHANNELS, NO_FREQUENCY, AudioTrackType.MAIN ) ); } } return AudioTracks.from(audioTracks); } boolean selectAudioTrack(MediaPlayer mediaPlayer, PlayerAudioTrack playerAudioTrack) { if (mediaPlayer == null) { throw new IllegalStateException("You can only call selectAudioTrack() when video is prepared."); } mediaPlayer.selectTrack(playerAudioTrack.groupIndex()); return true; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/AndroidMediaPlayerFacade.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import android.content.Context; import android.media.AudioManager; import android.media.MediaPlayer; import android.net.Uri; import android.support.annotation.Nullable; import android.view.Surface; import android.view.SurfaceHolder; import com.novoda.noplayer.internal.mediaplayer.PlaybackStateChecker.PlaybackState; import com.novoda.noplayer.internal.mediaplayer.forwarder.MediaPlayerForwarder; import com.novoda.noplayer.internal.utils.NoPlayerLog; import com.novoda.noplayer.internal.utils.Optional; import com.novoda.noplayer.model.AudioTracks; import com.novoda.noplayer.model.Either; import com.novoda.noplayer.model.PlayerAudioTrack; import com.novoda.noplayer.model.PlayerSubtitleTrack; import com.novoda.noplayer.model.PlayerVideoTrack; import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Map; import static com.novoda.noplayer.internal.mediaplayer.PlaybackStateChecker.PlaybackState.IDLE; import static com.novoda.noplayer.internal.mediaplayer.PlaybackStateChecker.PlaybackState.PAUSED; import static com.novoda.noplayer.internal.mediaplayer.PlaybackStateChecker.PlaybackState.PLAYING; // Not much we can do, wrapping MediaPlayer is a lot of work @SuppressWarnings("PMD.GodClass") class AndroidMediaPlayerFacade { private static final Map NO_HEADERS = null; private final Context context; private final MediaPlayerForwarder forwarder; private final AudioManager audioManager; private final AndroidMediaPlayerAudioTrackSelector trackSelector; private final PlaybackStateChecker playbackStateChecker; private final MediaPlayerCreator mediaPlayerCreator; private PlaybackState currentState = IDLE; private int currentBufferPercentage; private float volume = 1.0f; @Nullable private MediaPlayer mediaPlayer; static AndroidMediaPlayerFacade newInstance(Context context, MediaPlayerForwarder forwarder) { TrackInfosFactory trackInfosFactory = new TrackInfosFactory(); AndroidMediaPlayerAudioTrackSelector trackSelector = new AndroidMediaPlayerAudioTrackSelector(trackInfosFactory); PlaybackStateChecker playbackStateChecker = new PlaybackStateChecker(); AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); MediaPlayerCreator mediaPlayerCreator = new MediaPlayerCreator(); return new AndroidMediaPlayerFacade(context, forwarder, audioManager, trackSelector, playbackStateChecker, mediaPlayerCreator); } AndroidMediaPlayerFacade(Context context, MediaPlayerForwarder forwarder, AudioManager audioManager, AndroidMediaPlayerAudioTrackSelector trackSelector, PlaybackStateChecker playbackStateChecker, MediaPlayerCreator mediaPlayerCreator) { this.context = context; this.forwarder = forwarder; this.audioManager = audioManager; this.trackSelector = trackSelector; this.playbackStateChecker = playbackStateChecker; this.mediaPlayerCreator = mediaPlayerCreator; } void prepareVideo(Uri videoUri, Either surface) { requestAudioFocus(); release(); try { currentState = PlaybackState.PREPARING; mediaPlayer = createAndBindMediaPlayer(surface, videoUri); mediaPlayer.prepareAsync(); } catch (IOException | IllegalArgumentException | IllegalStateException ex) { reportCreationError(ex, videoUri); } } private void requestAudioFocus() { audioManager.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); } private MediaPlayer createAndBindMediaPlayer(Either surface, Uri videoUri) throws IOException, IllegalStateException, IllegalArgumentException { MediaPlayer mediaPlayer = mediaPlayerCreator.createMediaPlayer(); mediaPlayer.setOnPreparedListener(internalPreparedListener); mediaPlayer.setOnVideoSizeChangedListener(internalSizeChangedListener); mediaPlayer.setOnCompletionListener(internalCompletionListener); mediaPlayer.setOnErrorListener(internalErrorListener); mediaPlayer.setOnBufferingUpdateListener(internalBufferingUpdateListener); mediaPlayer.setDataSource(context, videoUri, NO_HEADERS); attachSurface(mediaPlayer, surface); mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mediaPlayer.setScreenOnWhilePlaying(true); currentBufferPercentage = 0; volume = 1.0f; return mediaPlayer; } private void reportCreationError(Exception ex, Uri videoUri) { NoPlayerLog.w(ex, "Unable to open content: " + videoUri); currentState = PlaybackState.ERROR; internalErrorListener.onError(mediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); } private final MediaPlayer.OnVideoSizeChangedListener internalSizeChangedListener = new MediaPlayer.OnVideoSizeChangedListener() { @Override public void onVideoSizeChanged(MediaPlayer mp, int width, int height) { MediaPlayer.OnVideoSizeChangedListener onVideoSizeChangedForwarder = forwarder.onSizeChangedListener(); if (onVideoSizeChangedForwarder == null) { throw new IllegalStateException("Should bind a OnVideoSizeChangedListener. Cannot forward events."); } onVideoSizeChangedForwarder.onVideoSizeChanged(mp, width, height); } }; private final MediaPlayer.OnPreparedListener internalPreparedListener = new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { currentState = PlaybackState.PREPARED; MediaPlayer.OnPreparedListener onPreparedForwarder = forwarder.onPreparedListener(); if (onPreparedForwarder == null) { throw new IllegalStateException("Should bind a OnPreparedListener. Cannot forward events."); } onPreparedForwarder.onPrepared(mediaPlayer); } }; private final MediaPlayer.OnCompletionListener internalCompletionListener = new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { currentState = PlaybackState.COMPLETED; MediaPlayer.OnCompletionListener onCompletionForwarder = forwarder.onCompletionListener(); if (onCompletionForwarder == null) { throw new IllegalStateException("Should bind a OnCompletionListener. Cannot forward events."); } onCompletionForwarder.onCompletion(mediaPlayer); } }; private final MediaPlayer.OnErrorListener internalErrorListener = new MediaPlayer.OnErrorListener() { @Override public boolean onError(MediaPlayer mp, int what, int extra) { NoPlayerLog.d("Error: " + what + "," + extra); currentState = PlaybackState.ERROR; MediaPlayer.OnErrorListener onErrorForwarder = forwarder.onErrorListener(); if (onErrorForwarder == null) { throw new IllegalStateException("Should bind a OnErrorListener. Cannot forward events."); } return onErrorForwarder.onError(mediaPlayer, what, extra); } }; private final MediaPlayer.OnBufferingUpdateListener internalBufferingUpdateListener = new MediaPlayer.OnBufferingUpdateListener() { @Override public void onBufferingUpdate(MediaPlayer mp, int percent) { currentBufferPercentage = percent; } }; void release() { if (hasPlayer()) { mediaPlayer.reset(); mediaPlayer.release(); mediaPlayer = null; currentState = IDLE; } } void start(Either surface) throws IllegalStateException { assertIsInPlaybackState(); attachSurface(mediaPlayer, surface); currentState = PLAYING; mediaPlayer.start(); } private void attachSurface(final MediaPlayer mediaPlayer, Either surface) { Either.Consumer setSurface = new Either.Consumer() { @Override public void accept(Surface value) { mediaPlayer.setSurface(value); } }; Either.Consumer setDisplay = new Either.Consumer() { @Override public void accept(SurfaceHolder value) { mediaPlayer.setDisplay(value); } }; surface.apply(setSurface, setDisplay); } void pause() throws IllegalStateException { assertIsInPlaybackState(); if (isPlaying()) { mediaPlayer.pause(); currentState = PAUSED; } } int mediaDurationInMillis() throws IllegalStateException { assertIsInPlaybackState(); return mediaPlayer.getDuration(); } int currentPositionInMillis() throws IllegalStateException { assertIsInPlaybackState(); return mediaPlayer.getCurrentPosition(); } void seekTo(long positionInMillis) throws IllegalStateException { assertIsInPlaybackState(); mediaPlayer.seekTo((int) positionInMillis); } boolean isPlaying() { return playbackStateChecker.isPlaying(mediaPlayer, currentState); } int getBufferPercentage() throws IllegalStateException { assertIsInPlaybackState(); return currentBufferPercentage; } AudioTracks getAudioTracks() throws IllegalStateException { assertIsInPlaybackState(); return trackSelector.getAudioTracks(mediaPlayer); } boolean selectAudioTrack(PlayerAudioTrack playerAudioTrack) throws IllegalStateException { assertIsInPlaybackState(); return trackSelector.selectAudioTrack(mediaPlayer, playerAudioTrack); } boolean clearAudioTrackSelection() { assertIsInPlaybackState(); NoPlayerLog.w("Tried to clear audio track selection but has not been implemented for MediaPlayer."); return false; } void setOnSeekCompleteListener(MediaPlayer.OnSeekCompleteListener seekToResettingSeekListener) throws IllegalStateException { assertIsInPlaybackState(); mediaPlayer.setOnSeekCompleteListener(seekToResettingSeekListener); } boolean hasPlayedContent() { return hasPlayer(); } private boolean hasPlayer() { return mediaPlayer != null; } boolean clearSubtitleTrack() throws IllegalStateException { assertIsInPlaybackState(); NoPlayerLog.w("Tried to hide subtitle track but has not been implemented for MediaPlayer."); return false; } boolean selectSubtitleTrack(PlayerSubtitleTrack subtitleTrack) throws IllegalStateException { assertIsInPlaybackState(); NoPlayerLog.w("Tried to select subtitle track but has not been implemented for MediaPlayer."); return false; } List getSubtitleTracks() throws IllegalStateException { assertIsInPlaybackState(); NoPlayerLog.w("Tried to get subtitle tracks but has not been implemented for MediaPlayer."); return Collections.emptyList(); } private void assertIsInPlaybackState() throws IllegalStateException { if (!playbackStateChecker.isInPlaybackState(mediaPlayer, currentState)) { throw new IllegalStateException("Video must be loaded and not in an error state before trying to interact with the player"); } } Optional getSelectedVideoTrack() { assertIsInPlaybackState(); NoPlayerLog.w("Tried to get the currently playing video track but has not been implemented for MediaPlayer."); return Optional.absent(); } List getVideoTracks() { assertIsInPlaybackState(); NoPlayerLog.w("Tried to get video tracks but has not been implemented for MediaPlayer."); return Collections.emptyList(); } boolean selectVideoTrack(PlayerVideoTrack videoTrack) { assertIsInPlaybackState(); NoPlayerLog.w("Tried to select a video track but has not been implemented for MediaPlayer."); return false; } boolean clearVideoTrackSelection() { assertIsInPlaybackState(); NoPlayerLog.w("Tried to clear video track selection but has not been implemented for MediaPlayer."); return false; } void setRepeating(boolean repeating) { assertIsInPlaybackState(); mediaPlayer.setLooping(repeating); } void setVolume(float volume) { assertIsInPlaybackState(); this.volume = volume; mediaPlayer.setVolume(volume, volume); } float getVolume() { assertIsInPlaybackState(); return volume; } void clearMaxVideoBitrate() { assertIsInPlaybackState(); NoPlayerLog.w("Tried to clear max video bitrate but has not been implemented for MediaPlayer."); } void setMaxVideoBitrate(int maxVideoBitrate) { assertIsInPlaybackState(); NoPlayerLog.w("Tried to set max video bitrate but has not been implemented for MediaPlayer."); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/AndroidMediaPlayerImpl.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import android.media.MediaPlayer; import android.net.Uri; import android.view.Surface; import android.view.SurfaceHolder; import android.view.View; import com.novoda.noplayer.Listeners; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.Options; import com.novoda.noplayer.PlayerInformation; import com.novoda.noplayer.PlayerState; import com.novoda.noplayer.PlayerSurfaceHolder; import com.novoda.noplayer.PlayerView; import com.novoda.noplayer.SurfaceRequester; import com.novoda.noplayer.internal.Heart; import com.novoda.noplayer.internal.listeners.PlayerListenersHolder; import com.novoda.noplayer.internal.mediaplayer.forwarder.MediaPlayerForwarder; import com.novoda.noplayer.internal.utils.Optional; import com.novoda.noplayer.model.AudioTracks; import com.novoda.noplayer.model.Either; import com.novoda.noplayer.model.LoadTimeout; import com.novoda.noplayer.model.PlayerAudioTrack; import com.novoda.noplayer.model.PlayerSubtitleTrack; import com.novoda.noplayer.model.PlayerVideoTrack; import com.novoda.noplayer.model.Timeout; import java.util.ArrayList; import java.util.List; // Not much we can do, wrapping MediaPlayer is a lot of work @SuppressWarnings("PMD.GodClass") class AndroidMediaPlayerImpl implements NoPlayer { private static final long NO_SEEK_TO_POSITION = -1; private static final long INITIAL_PLAY_SEEK_DELAY_IN_MILLIS = 500; private final List surfaceHolderRequesterCallbacks = new ArrayList<>(); private final MediaPlayerInformation mediaPlayerInformation; private final AndroidMediaPlayerFacade mediaPlayer; private final MediaPlayerForwarder forwarder; private final CheckBufferHeartbeatCallback bufferHeartbeatCallback; private final DelayedActionExecutor delayedActionExecutor; private final Heart heart; private final PlayerListenersHolder listenersHolder; private final LoadTimeout loadTimeout; private final BuggyVideoDriverPreventer buggyVideoDriverPreventer; private int videoWidth; private int videoHeight; private long seekToPositionInMillis = NO_SEEK_TO_POSITION; private boolean seekingWithIntentToPlay; private SurfaceRequester surfaceRequester; private View containerView; @SuppressWarnings("checkstyle:ParameterNumber") // We cannot really group these any further AndroidMediaPlayerImpl(MediaPlayerInformation mediaPlayerInformation, AndroidMediaPlayerFacade mediaPlayer, MediaPlayerForwarder forwarder, PlayerListenersHolder listenersHolder, CheckBufferHeartbeatCallback bufferHeartbeatCallback, LoadTimeout loadTimeout, Heart heart, DelayedActionExecutor delayedActionExecutor, BuggyVideoDriverPreventer buggyVideoDriverPreventer) { this.mediaPlayerInformation = mediaPlayerInformation; this.mediaPlayer = mediaPlayer; this.forwarder = forwarder; this.listenersHolder = listenersHolder; this.bufferHeartbeatCallback = bufferHeartbeatCallback; this.loadTimeout = loadTimeout; this.heart = heart; this.delayedActionExecutor = delayedActionExecutor; this.buggyVideoDriverPreventer = buggyVideoDriverPreventer; } void initialise() { forwarder.bind(listenersHolder.getPreparedListeners(), this); forwarder.bind(listenersHolder.getBufferStateListeners(), listenersHolder.getErrorListeners()); forwarder.bind(listenersHolder.getCompletionListeners(), listenersHolder.getStateChangedListeners()); forwarder.bind(listenersHolder.getVideoSizeChangedListeners()); forwarder.bind(listenersHolder.getInfoListeners()); bufferHeartbeatCallback.bind(forwarder.onHeartbeatListener()); heart.bind(new Heart.Heartbeat(listenersHolder.getHeartbeatCallbacks(), this)); listenersHolder.addHeartbeatCallback(bufferHeartbeatCallback); listenersHolder.addPreparedListener(new PreparedListener() { @Override public void onPrepared(PlayerState playerState) { loadTimeout.cancel(); mediaPlayer.setOnSeekCompleteListener(seekToResettingSeekListener); } }); listenersHolder.addErrorListener(new ErrorListener() { @Override public void onError(PlayerError error) { reset(); } }); listenersHolder.addVideoSizeChangedListener(new VideoSizeChangedListener() { @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { videoWidth = width; videoHeight = height; } }); } private final MediaPlayer.OnSeekCompleteListener seekToResettingSeekListener = new MediaPlayer.OnSeekCompleteListener() { @Override public void onSeekComplete(MediaPlayer mp) { seekToPositionInMillis = NO_SEEK_TO_POSITION; if (seekingWithIntentToPlay || isPlaying()) { seekingWithIntentToPlay = false; play(); } } }; @Override public void setRepeating(boolean repeating) { mediaPlayer.setRepeating(repeating); } @Override public void setVolume(float volume) { mediaPlayer.setVolume(volume); } @Override public float getVolume() { return mediaPlayer.getVolume(); } @Override public Listeners getListeners() { return listenersHolder; } @Override public void play() throws IllegalStateException { heart.startBeatingHeart(); requestSurface(new SurfaceRequester.Callback() { @Override public void onSurfaceReady(Either surface) { mediaPlayer.start(surface); listenersHolder.getStateChangedListeners().onVideoPlaying(); } }); } @Override public void playAt(final long positionInMillis) throws IllegalStateException { if (playheadPositionInMillis() == positionInMillis) { play(); } else { requestSurface(new SurfaceRequester.Callback() { @Override public void onSurfaceReady(Either surface) { initialSeekWorkaround(surface, positionInMillis); } }); } } /** * Workaround to fix some devices (nexus 7 2013 in particular) from natively crashing the mediaplayer * by starting the mediaplayer before seeking it. */ private void initialSeekWorkaround(Either surface, final long initialPlayPositionInMillis) throws IllegalStateException { listenersHolder.getBufferStateListeners().onBufferStarted(); initialisePlaybackForSeeking(surface); delayedActionExecutor.performAfterDelay(new DelayedActionExecutor.Action() { @Override public void perform() { seekWithIntentToPlay(initialPlayPositionInMillis); } }, INITIAL_PLAY_SEEK_DELAY_IN_MILLIS); } private void initialisePlaybackForSeeking(Either surface) { mediaPlayer.start(surface); mediaPlayer.pause(); } private void requestSurface(SurfaceRequester.Callback callback) { if (surfaceRequester == null) { throw new IllegalStateException("Must attach a PlayerView before interacting with Player"); } surfaceHolderRequesterCallbacks.add(callback); surfaceRequester.requestSurface(callback); } private void seekWithIntentToPlay(long positionInMillis) throws IllegalStateException { seekingWithIntentToPlay = true; seekTo(positionInMillis); } @Override public boolean isPlaying() { return mediaPlayer.isPlaying(); } @Override public void seekTo(long positionInMillis) throws IllegalStateException { seekToPositionInMillis = positionInMillis; mediaPlayer.seekTo(positionInMillis); } @Override public void pause() throws IllegalStateException { mediaPlayer.pause(); if (heart.isBeating()) { heart.stopBeatingHeart(); heart.forceBeat(); } listenersHolder.getStateChangedListeners().onVideoPaused(); } @Override public void loadVideo(final Uri uri, final Options options) { if (mediaPlayer.hasPlayedContent()) { stop(); } assertPlayerViewIsAttached(); createSurfaceByShowingVideoContainer(); listenersHolder.getBufferStateListeners().onBufferStarted(); requestSurface(new SurfaceRequester.Callback() { @Override public void onSurfaceReady(Either surface) { mediaPlayer.prepareVideo(uri, surface); } }); } private void createSurfaceByShowingVideoContainer() { containerView.setVisibility(View.VISIBLE); } private void assertPlayerViewIsAttached() { if (containerView == null) { throw new IllegalStateException("A PlayerView must be attached in order to loadVideo"); } } @Override public void loadVideoWithTimeout(Uri uri, Options options, Timeout timeout, LoadTimeoutCallback loadTimeoutCallback) { loadTimeout.start(timeout, loadTimeoutCallback); loadVideo(uri, options); } @Override public long playheadPositionInMillis() throws IllegalStateException { return isSeeking() ? seekToPositionInMillis : mediaPlayer.currentPositionInMillis(); } private boolean isSeeking() { return seekToPositionInMillis != NO_SEEK_TO_POSITION; } @Override public long mediaDurationInMillis() throws IllegalStateException { return mediaPlayer.mediaDurationInMillis(); } @Override public int bufferPercentage() throws IllegalStateException { return mediaPlayer.getBufferPercentage(); } @Override public int videoWidth() { return videoWidth; } @Override public int videoHeight() { return videoHeight; } @Override public PlayerInformation getPlayerInformation() { return mediaPlayerInformation; } @Override public void attach(PlayerView playerView) { containerView = playerView.getContainerView(); buggyVideoDriverPreventer.preventVideoDriverBug(this, containerView); listenersHolder.addVideoSizeChangedListener(playerView.getVideoSizeChangedListener()); listenersHolder.addStateChangedListener(playerView.getStateChangedListener()); PlayerSurfaceHolder playerSurfaceHolder = playerView.getPlayerSurfaceHolder(); surfaceRequester = playerSurfaceHolder.getSurfaceRequester(); } @Override public void detach(PlayerView playerView) { clearSurfaceHolderCallbacks(); listenersHolder.removeStateChangedListener(playerView.getStateChangedListener()); listenersHolder.removeVideoSizeChangedListener(playerView.getVideoSizeChangedListener()); buggyVideoDriverPreventer.clear(playerView.getContainerView()); surfaceRequester = null; containerView = null; } private void clearSurfaceHolderCallbacks() { for (SurfaceRequester.Callback callback : surfaceHolderRequesterCallbacks) { surfaceRequester.removeCallback(callback); } surfaceHolderRequesterCallbacks.clear(); } @Override public boolean selectAudioTrack(PlayerAudioTrack audioTrack) throws IllegalStateException { return mediaPlayer.selectAudioTrack(audioTrack); } @Override public boolean clearAudioTrackSelection() throws IllegalStateException { return mediaPlayer.clearAudioTrackSelection(); } @Override public boolean showSubtitleTrack(PlayerSubtitleTrack subtitleTrack) throws IllegalStateException { return mediaPlayer.selectSubtitleTrack(subtitleTrack); } @Override public boolean hideSubtitleTrack() throws IllegalStateException { return mediaPlayer.clearSubtitleTrack(); } @Override public AudioTracks getAudioTracks() throws IllegalStateException { return mediaPlayer.getAudioTracks(); } @Override public boolean selectVideoTrack(PlayerVideoTrack videoTrack) throws IllegalStateException { return mediaPlayer.selectVideoTrack(videoTrack); } @Override public Optional getSelectedVideoTrack() throws IllegalStateException { return mediaPlayer.getSelectedVideoTrack(); } @Override public boolean clearVideoTrackSelection() throws IllegalStateException { return mediaPlayer.clearVideoTrackSelection(); } @Override public List getVideoTracks() throws IllegalStateException { return mediaPlayer.getVideoTracks(); } @Override public List getSubtitleTracks() throws IllegalStateException { return mediaPlayer.getSubtitleTracks(); } @Override public void clearMaxVideoBitrate() { mediaPlayer.clearMaxVideoBitrate(); } @Override public void setMaxVideoBitrate(int maxVideoBitrate) { mediaPlayer.setMaxVideoBitrate(maxVideoBitrate); } @Override public void stop() { reset(); listenersHolder.getStateChangedListeners().onVideoStopped(); } @Override public void release() { stop(); listenersHolder.clear(); } private void reset() { delayedActionExecutor.clearAllActions(); listenersHolder.resetState(); loadTimeout.cancel(); heart.stopBeatingHeart(); mediaPlayer.release(); destroySurfaceByHidingVideoContainer(); } private void destroySurfaceByHidingVideoContainer() { if (containerView != null) { containerView.setVisibility(View.GONE); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/AndroidMediaPlayerType.java ================================================ package com.novoda.noplayer.internal.mediaplayer; enum AndroidMediaPlayerType { AWESOME("AwesomePlayer"), NU("NuPlayer"), UNKNOWN("Unknown"); private final String name; AndroidMediaPlayerType(String name) { this.name = name; } String getName() { return name; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/BuggyVideoDriverPreventer.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import android.view.View; import com.novoda.noplayer.NoPlayer; /** * The intent for this component is to workaround a buggy video driver affecting AwesomePlayer on Nexus 5. * After inserting the headphones the screen goes black, causing layout changes, after recovering * from the freeze subsequent calls to {@link android.media.MediaPlayer#pause()} are ignored, the internal status machine got corrupted. *

* It can be workaround by forcing {@link android.media.MediaPlayer#start()} when it was already playing. */ class BuggyVideoDriverPreventer { private final MediaPlayerTypeReader mediaPlayerTypeReader; private OnPotentialBuggyDriverLayoutListener preventerListener; BuggyVideoDriverPreventer(MediaPlayerTypeReader mediaPlayerTypeReader) { this.mediaPlayerTypeReader = mediaPlayerTypeReader; } void preventVideoDriverBug(NoPlayer player, View containerView) { if (videoDriverCanBeBuggy()) { attemptToCorrectMediaPlayerStatus(player, containerView); } } private boolean videoDriverCanBeBuggy() { return mediaPlayerTypeReader.getPlayerType() == AndroidMediaPlayerType.AWESOME; } private void attemptToCorrectMediaPlayerStatus(NoPlayer player, View containerView) { preventerListener = new OnPotentialBuggyDriverLayoutListener(player); containerView.addOnLayoutChangeListener(preventerListener); } void clear(View containerView) { containerView.removeOnLayoutChangeListener(preventerListener); preventerListener = null; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/CheckBufferHeartbeatCallback.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import com.novoda.noplayer.NoPlayer; public class CheckBufferHeartbeatCallback implements NoPlayer.HeartbeatCallback { private static final int FORCED_BUFFERING_BEATS_THRESHOLD = 4; private BufferListener bufferListener = BufferListener.NULL_IMPL; private long previousPositionInMillis = -1; private int beatsPlayed; public void bind(BufferListener bufferListener) { this.bufferListener = bufferListener; } @Override public void onBeat(NoPlayer player) { if (mediaPlayerIsUnavailable(player)) { stopBuffering(); return; } long currentPositionInMillis = player.playheadPositionInMillis(); if (positionNotUpdating(currentPositionInMillis)) { beatsPlayed = 0; startBuffering(); } else { previousPositionInMillis = currentPositionInMillis; beatsPlayed++; if (beatsPlayed > FORCED_BUFFERING_BEATS_THRESHOLD) { stopBuffering(); } } } private boolean positionNotUpdating(long currentPositionInMillis) { return currentPositionInMillis == previousPositionInMillis; } private void stopBuffering() { bufferListener.onBufferComplete(); } private void startBuffering() { bufferListener.onBufferStart(); } private boolean mediaPlayerIsUnavailable(NoPlayer player) { try { return !player.isPlaying(); } catch (IllegalStateException e) { // The mediaplayer has not been initialized or has been released return true; } } public interface BufferListener { void onBufferStart(); void onBufferComplete(); BufferListener NULL_IMPL = new BufferListener() { @Override public void onBufferStart() { // do nothing } @Override public void onBufferComplete() { // do nothing } }; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/DelayedActionExecutor.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import android.os.Handler; import java.util.Iterator; import java.util.Map; class DelayedActionExecutor { private final Handler handler; private final Map runnables; DelayedActionExecutor(Handler handler, Map runnables) { this.handler = handler; this.runnables = runnables; } void performAfterDelay(final Action action, long delayInMillis) { Runnable actionRunnable = new Runnable() { @Override public void run() { action.perform(); runnables.remove(action); } }; runnables.put(action, actionRunnable); handler.postDelayed(actionRunnable, delayInMillis); } void clearAllActions() { Iterator> it = runnables.entrySet().iterator(); while (it.hasNext()) { Map.Entry entry = it.next(); handler.removeCallbacks(entry.getValue()); it.remove(); } } interface Action { void perform(); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/ErrorFactory.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import android.media.MediaPlayer; import com.novoda.noplayer.DetailErrorType; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.NoPlayerError; import com.novoda.noplayer.PlayerErrorType; public final class ErrorFactory { private ErrorFactory() { // no instances } @SuppressWarnings({"PMD.StdCyclomaticComplexity", "PMD.CyclomaticComplexity"}) public static NoPlayer.PlayerError createErrorFrom(int type, int extra) { String message = String.valueOf(extra); switch (type) { case MediaPlayer.MEDIA_ERROR_IO: return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.MEDIA_PLAYER_IO, message); case MediaPlayer.MEDIA_ERROR_MALFORMED: return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.MEDIA_PLAYER_MALFORMED, message); case MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK: return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.MEDIA_PLAYER_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK, message); case MediaPlayer.MEDIA_INFO_NOT_SEEKABLE: return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.MEDIA_PLAYER_INFO_NOT_SEEKABLE, message); case MediaPlayer.MEDIA_INFO_SUBTITLE_TIMED_OUT: return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.MEDIA_PLAYER_SUBTITLE_TIMED_OUT, message); case MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE: return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.MEDIA_PLAYER_UNSUPPORTED_SUBTITLE, message); case MediaPlayer.MEDIA_ERROR_SERVER_DIED: return new NoPlayerError(PlayerErrorType.DRM, DetailErrorType.MEDIA_PLAYER_SERVER_DIED, message); case MediaPlayer.PREPARE_DRM_STATUS_PREPARATION_ERROR: return new NoPlayerError(PlayerErrorType.DRM, DetailErrorType.MEDIA_PLAYER_PREPARE_DRM_STATUS_PREPARATION_ERROR, message); case MediaPlayer.PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR: return new NoPlayerError(PlayerErrorType.DRM, DetailErrorType.MEDIA_PLAYER_PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR, message); case MediaPlayer.PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR: return new NoPlayerError(PlayerErrorType.DRM, DetailErrorType.MEDIA_PLAYER_PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR, message); case MediaPlayer.MEDIA_ERROR_TIMED_OUT: return new NoPlayerError(PlayerErrorType.CONNECTIVITY, DetailErrorType.MEDIA_PLAYER_TIMED_OUT, message); case MediaPlayer.MEDIA_INFO_AUDIO_NOT_PLAYING: return new NoPlayerError(PlayerErrorType.RENDERER_DECODER, DetailErrorType.MEDIA_PLAYER_INFO_AUDIO_NOT_PLAYING, message); case MediaPlayer.MEDIA_INFO_BAD_INTERLEAVING: return new NoPlayerError(PlayerErrorType.RENDERER_DECODER, DetailErrorType.MEDIA_PLAYER_BAD_INTERLEAVING, message); case MediaPlayer.MEDIA_INFO_VIDEO_NOT_PLAYING: return new NoPlayerError(PlayerErrorType.RENDERER_DECODER, DetailErrorType.MEDIA_PLAYER_INFO_VIDEO_NOT_PLAYING, message); case MediaPlayer.MEDIA_INFO_VIDEO_TRACK_LAGGING: return new NoPlayerError(PlayerErrorType.RENDERER_DECODER, DetailErrorType.MEDIA_PLAYER_INFO_VIDEO_TRACK_LAGGING, message); default: return new NoPlayerError(PlayerErrorType.UNKNOWN, DetailErrorType.MEDIA_PLAYER_UNKNOWN, message); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/ErrorFormatter.java ================================================ package com.novoda.noplayer.internal.mediaplayer; final class ErrorFormatter { private ErrorFormatter() { } static String formatMessage(int type, int extra) { return "Type: " + type + ", " + "Extra: " + extra; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/MediaPlayerCreator.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import android.media.MediaPlayer; class MediaPlayerCreator { MediaPlayer createMediaPlayer() { return new MediaPlayer(); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/MediaPlayerInformation.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import android.os.Build; import com.novoda.noplayer.PlayerInformation; import com.novoda.noplayer.PlayerType; class MediaPlayerInformation implements PlayerInformation { private final MediaPlayerTypeReader mediaPlayerTypeReader; MediaPlayerInformation(MediaPlayerTypeReader mediaPlayerTypeReader) { this.mediaPlayerTypeReader = mediaPlayerTypeReader; } @Override public PlayerType getPlayerType() { return PlayerType.MEDIA_PLAYER; } @Override public String getVersion() { return Build.VERSION.RELEASE; } @Override public String getName() { return "MediaPlayer: " + mediaPlayerTypeReader.getPlayerType().getName(); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/MediaPlayerTypeReader.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import android.os.Build; class MediaPlayerTypeReader { private static final String PROP_USE_NU_PLAYER = "media.stagefright.use-nuplayer"; private static final String PROP_USE_AWESOME_PLAYER_PERSIST = "persist.sys.media.use-awesome"; private static final String PROP_USE_AWESOME_PLAYER_MEDIA = "media.stagefright.use-awesome"; private final int deviceOSVersion; private final SystemProperties systemProperties; MediaPlayerTypeReader(SystemProperties systemProperties, int deviceOSVersion) { this.systemProperties = systemProperties; this.deviceOSVersion = deviceOSVersion; } AndroidMediaPlayerType getPlayerType() { AndroidMediaPlayerType playerType; try { playerType = getMediaPlayerType(); } catch (SystemProperties.MissingSystemPropertiesException e) { playerType = AndroidMediaPlayerType.UNKNOWN; } return playerType; } private AndroidMediaPlayerType getMediaPlayerType() throws SystemProperties.MissingSystemPropertiesException { return deviceOSVersion >= Build.VERSION_CODES.LOLLIPOP ? getPlayerTypeLollipop() : getPlayerTypePreLollipop(); } private AndroidMediaPlayerType getPlayerTypeLollipop() throws SystemProperties.MissingSystemPropertiesException { // NuPlayer is enabled if property is false or absent // http://androidxref.com/5.0.0_r2/xref/frameworks/av/media/libmediaplayerservice/MediaPlayerFactory.cpp#63 boolean isAwesomePlayerEnabled = getBooleanProp(PROP_USE_AWESOME_PLAYER_PERSIST) || getBooleanProp(PROP_USE_AWESOME_PLAYER_MEDIA); return isAwesomePlayerEnabled ? AndroidMediaPlayerType.AWESOME : AndroidMediaPlayerType.NU; } private AndroidMediaPlayerType getPlayerTypePreLollipop() throws SystemProperties.MissingSystemPropertiesException { // NuPlayer is disabled if property is false or absent // http://androidxref.com/4.4.4_r1/xref/frameworks/av/media/libmediaplayerservice/MediaPlayerFactory.cpp#63 return getBooleanProp(PROP_USE_NU_PLAYER) ? AndroidMediaPlayerType.NU : AndroidMediaPlayerType.AWESOME; } private boolean getBooleanProp(String prop) throws SystemProperties.MissingSystemPropertiesException { String value = systemProperties.get(prop); return "true".equalsIgnoreCase(value) || "1".equals(value); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/NoPlayerMediaPlayerCreator.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import android.content.Context; import android.os.Build; import android.os.Handler; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.internal.Heart; import com.novoda.noplayer.internal.SystemClock; import com.novoda.noplayer.internal.listeners.PlayerListenersHolder; import com.novoda.noplayer.internal.mediaplayer.forwarder.MediaPlayerForwarder; import com.novoda.noplayer.model.LoadTimeout; import java.util.HashMap; public class NoPlayerMediaPlayerCreator { private final InternalCreator internalCreator; public static NoPlayerMediaPlayerCreator newInstance(Handler handler) { InternalCreator internalCreator = new InternalCreator(handler); return new NoPlayerMediaPlayerCreator(internalCreator); } NoPlayerMediaPlayerCreator(InternalCreator internalCreator) { this.internalCreator = internalCreator; } public NoPlayer createMediaPlayer(Context context) { AndroidMediaPlayerImpl player = internalCreator.create(context); player.initialise(); return player; } static class InternalCreator { private final Handler handler; InternalCreator(Handler handler) { this.handler = handler; } public AndroidMediaPlayerImpl create(Context context) { LoadTimeout loadTimeout = new LoadTimeout(new SystemClock(), handler); MediaPlayerForwarder forwarder = new MediaPlayerForwarder(); AndroidMediaPlayerFacade facade = AndroidMediaPlayerFacade.newInstance(context, forwarder); PlayerListenersHolder listenersHolder = new PlayerListenersHolder(); CheckBufferHeartbeatCallback bufferHeartbeatCallback = new CheckBufferHeartbeatCallback(); Heart heart = Heart.newInstance(handler); MediaPlayerTypeReader mediaPlayerTypeReader = new MediaPlayerTypeReader(new SystemProperties(), Build.VERSION.SDK_INT); DelayedActionExecutor delayedActionExecutor = new DelayedActionExecutor(handler, new HashMap()); BuggyVideoDriverPreventer preventer = new BuggyVideoDriverPreventer(mediaPlayerTypeReader); MediaPlayerInformation mediaPlayerInformation = new MediaPlayerInformation(mediaPlayerTypeReader); return new AndroidMediaPlayerImpl( mediaPlayerInformation, facade, forwarder, listenersHolder, bufferHeartbeatCallback, loadTimeout, heart, delayedActionExecutor, preventer ); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/NoPlayerTrackInfo.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import android.media.MediaPlayer; class NoPlayerTrackInfo { private final MediaPlayer.TrackInfo trackInfo; NoPlayerTrackInfo(MediaPlayer.TrackInfo trackInfo) { this.trackInfo = trackInfo; } int type() { return trackInfo.getTrackType(); } String language() { return trackInfo.getLanguage(); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/NoPlayerTrackInfos.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import java.util.List; class NoPlayerTrackInfos { private final List trackInfos; NoPlayerTrackInfos(List trackInfos) { this.trackInfos = trackInfos; } NoPlayerTrackInfo get(int index) { return trackInfos.get(index); } int size() { return trackInfos.size(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } NoPlayerTrackInfos that = (NoPlayerTrackInfos) o; return trackInfos != null ? trackInfos.equals(that.trackInfos) : that.trackInfos == null; } @Override public int hashCode() { return trackInfos != null ? trackInfos.hashCode() : 0; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/OnPotentialBuggyDriverLayoutListener.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import android.view.View; import com.novoda.noplayer.NoPlayer; class OnPotentialBuggyDriverLayoutListener implements View.OnLayoutChangeListener { private final NoPlayer player; OnPotentialBuggyDriverLayoutListener(NoPlayer player) { this.player = player; } @SuppressWarnings("checkstyle:parameternumber") // Checkstyle should not complain about interface methods. No way to workaround this. @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { if (statusMightBeCorrupted()) { forceAlignNativeMediaPlayerStatus(); } } private boolean statusMightBeCorrupted() { return player.isPlaying(); } private void forceAlignNativeMediaPlayerStatus() { player.play(); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/PlaybackStateChecker.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import android.media.MediaPlayer; import static com.novoda.noplayer.internal.mediaplayer.PlaybackStateChecker.PlaybackState.ERROR; import static com.novoda.noplayer.internal.mediaplayer.PlaybackStateChecker.PlaybackState.IDLE; import static com.novoda.noplayer.internal.mediaplayer.PlaybackStateChecker.PlaybackState.PREPARING; class PlaybackStateChecker { boolean isPlaying(MediaPlayer mediaPlayer, PlaybackState playbackState) { return isInPlaybackState(mediaPlayer, playbackState) && mediaPlayer.isPlaying(); } boolean isInPlaybackState(MediaPlayer mediaPlayer, PlaybackState playbackState) { return mediaPlayer != null && playbackState != ERROR && playbackState != IDLE && playbackState != PREPARING; } enum PlaybackState { ERROR, IDLE, PREPARING, PREPARED, PLAYING, PAUSED, COMPLETED } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/SystemProperties.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import android.annotation.SuppressLint; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; class SystemProperties { private static final String SYSTEM_PROPERTIES_CLASS = "android.os.SystemProperties"; private static final String SYSTEM_PROPERTIES_METHOD_GET = "get"; private static final Object STATIC_CLASS_INSTANCE = null; @SuppressLint("PrivateApi") // This method uses reflection to call android.os.SystemProperties.get(String) since the class is hidden String get(String key) throws MissingSystemPropertiesException { try { Class systemProperties = Class.forName(SYSTEM_PROPERTIES_CLASS); Method getMethod = systemProperties.getMethod(SYSTEM_PROPERTIES_METHOD_GET, String.class); return (String) getMethod.invoke(STATIC_CLASS_INSTANCE, key); } catch (ClassNotFoundException e) { throw new MissingSystemPropertiesException(e); } catch (NoSuchMethodException e) { throw new MissingSystemPropertiesException(e); } catch (InvocationTargetException e) { throw new MissingSystemPropertiesException(e); } catch (IllegalAccessException e) { throw new MissingSystemPropertiesException(e); } } static class MissingSystemPropertiesException extends Exception { MissingSystemPropertiesException(Exception e) { super(e); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/TrackInfosFactory.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import android.media.MediaPlayer; import java.util.ArrayList; import java.util.List; class TrackInfosFactory { NoPlayerTrackInfos createFrom(MediaPlayer mediaPlayer) { MediaPlayer.TrackInfo[] mediaPlayerTrackInfos = mediaPlayer.getTrackInfo(); List trackInfos = new ArrayList<>(mediaPlayerTrackInfos.length); for (MediaPlayer.TrackInfo mediaPlayerTrackInfo : mediaPlayerTrackInfos) { trackInfos.add(new NoPlayerTrackInfo(mediaPlayerTrackInfo)); } return new NoPlayerTrackInfos(trackInfos); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/BufferHeartbeatListener.java ================================================ package com.novoda.noplayer.internal.mediaplayer.forwarder; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.internal.mediaplayer.CheckBufferHeartbeatCallback; class BufferHeartbeatListener implements CheckBufferHeartbeatCallback.BufferListener { private final NoPlayer.BufferStateListener bufferStateListener; BufferHeartbeatListener(NoPlayer.BufferStateListener bufferStateListener) { this.bufferStateListener = bufferStateListener; } @Override public void onBufferStart() { bufferStateListener.onBufferStarted(); } @Override public void onBufferComplete() { bufferStateListener.onBufferCompleted(); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/BufferInfoForwarder.java ================================================ package com.novoda.noplayer.internal.mediaplayer.forwarder; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.internal.mediaplayer.CheckBufferHeartbeatCallback; import java.util.HashMap; class BufferInfoForwarder implements CheckBufferHeartbeatCallback.BufferListener { private final NoPlayer.InfoListener infoListener; BufferInfoForwarder(NoPlayer.InfoListener infoListener) { this.infoListener = infoListener; } @Override public void onBufferStart() { HashMap callingMethodParameters = new HashMap<>(); infoListener.onNewInfo("onBufferStart", callingMethodParameters); } @Override public void onBufferComplete() { HashMap callingMethodParameters = new HashMap<>(); infoListener.onNewInfo("onBufferStart", callingMethodParameters); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/BufferOnPreparedListener.java ================================================ package com.novoda.noplayer.internal.mediaplayer.forwarder; import android.media.MediaPlayer; import com.novoda.noplayer.NoPlayer; class BufferOnPreparedListener implements MediaPlayer.OnPreparedListener { private final NoPlayer.BufferStateListener bufferStateListener; BufferOnPreparedListener(NoPlayer.BufferStateListener bufferStateListener) { this.bufferStateListener = bufferStateListener; } @Override public void onPrepared(MediaPlayer mp) { bufferStateListener.onBufferCompleted(); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/CompletionForwarder.java ================================================ package com.novoda.noplayer.internal.mediaplayer.forwarder; import android.media.MediaPlayer; import com.novoda.noplayer.NoPlayer; class CompletionForwarder implements MediaPlayer.OnCompletionListener { private final NoPlayer.CompletionListener completionListener; CompletionForwarder(NoPlayer.CompletionListener completionListener) { this.completionListener = completionListener; } @Override public void onCompletion(MediaPlayer mp) { completionListener.onCompletion(); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/CompletionInfoForwarder.java ================================================ package com.novoda.noplayer.internal.mediaplayer.forwarder; import android.media.MediaPlayer; import com.novoda.noplayer.NoPlayer; import java.util.HashMap; class CompletionInfoForwarder implements MediaPlayer.OnCompletionListener { private final NoPlayer.InfoListener infoListener; CompletionInfoForwarder(NoPlayer.InfoListener infoListener) { this.infoListener = infoListener; } @Override public void onCompletion(MediaPlayer mp) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put("mp", String.valueOf(mp)); infoListener.onNewInfo("onCompletion", callingMethodParameters); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/CompletionStateChangedForwarder.java ================================================ package com.novoda.noplayer.internal.mediaplayer.forwarder; import android.media.MediaPlayer; import com.novoda.noplayer.NoPlayer; class CompletionStateChangedForwarder implements MediaPlayer.OnCompletionListener { private final NoPlayer.StateChangedListener stateChangedListener; CompletionStateChangedForwarder(NoPlayer.StateChangedListener stateChangedListener) { this.stateChangedListener = stateChangedListener; } @Override public void onCompletion(MediaPlayer mp) { stateChangedListener.onVideoStopped(); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/ErrorForwarder.java ================================================ package com.novoda.noplayer.internal.mediaplayer.forwarder; import android.media.MediaPlayer; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.internal.mediaplayer.ErrorFactory; class ErrorForwarder implements MediaPlayer.OnErrorListener { private final NoPlayer.BufferStateListener bufferStateListener; private final NoPlayer.ErrorListener errorListener; ErrorForwarder(NoPlayer.BufferStateListener bufferStateListener, NoPlayer.ErrorListener errorListener) { this.bufferStateListener = bufferStateListener; this.errorListener = errorListener; } @Override public boolean onError(MediaPlayer mp, int what, int extra) { bufferStateListener.onBufferCompleted(); NoPlayer.PlayerError error = ErrorFactory.createErrorFrom(what, extra); errorListener.onError(error); return true; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/ErrorInfoForwarder.java ================================================ package com.novoda.noplayer.internal.mediaplayer.forwarder; import android.media.MediaPlayer; import com.novoda.noplayer.NoPlayer; import java.util.HashMap; class ErrorInfoForwarder implements MediaPlayer.OnErrorListener { private final NoPlayer.InfoListener infoListener; ErrorInfoForwarder(NoPlayer.InfoListener infoListener) { this.infoListener = infoListener; } @Override public boolean onError(MediaPlayer mp, int what, int extra) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put("mp", String.valueOf(mp)); callingMethodParameters.put("what", String.valueOf(what)); callingMethodParameters.put("extra", String.valueOf(extra)); infoListener.onNewInfo("onError", callingMethodParameters); return false; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/HeartBeatListener.java ================================================ package com.novoda.noplayer.internal.mediaplayer.forwarder; import com.novoda.noplayer.internal.mediaplayer.CheckBufferHeartbeatCallback; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; class HeartBeatListener implements CheckBufferHeartbeatCallback.BufferListener { private final List listeners = new CopyOnWriteArrayList<>(); void add(CheckBufferHeartbeatCallback.BufferListener listener) { listeners.add(listener); } @Override public void onBufferStart() { for (CheckBufferHeartbeatCallback.BufferListener listener : listeners) { listener.onBufferStart(); } } @Override public void onBufferComplete() { for (CheckBufferHeartbeatCallback.BufferListener listener : listeners) { listener.onBufferComplete(); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/MediaPlayerCompletionListener.java ================================================ package com.novoda.noplayer.internal.mediaplayer.forwarder; import android.media.MediaPlayer; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; class MediaPlayerCompletionListener implements MediaPlayer.OnCompletionListener { private final List listeners = new CopyOnWriteArrayList<>(); void add(MediaPlayer.OnCompletionListener listener) { listeners.add(listener); } @Override public void onCompletion(MediaPlayer mp) { for (MediaPlayer.OnCompletionListener listener : listeners) { listener.onCompletion(mp); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/MediaPlayerErrorListener.java ================================================ package com.novoda.noplayer.internal.mediaplayer.forwarder; import android.media.MediaPlayer; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; class MediaPlayerErrorListener implements MediaPlayer.OnErrorListener { private final List listeners = new CopyOnWriteArrayList<>(); void add(MediaPlayer.OnErrorListener listener) { listeners.add(listener); } @Override public boolean onError(MediaPlayer mp, int what, int extra) { boolean handled = false; for (MediaPlayer.OnErrorListener listener : listeners) { handled = listener.onError(mp, what, extra) || handled; } return handled; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/MediaPlayerForwarder.java ================================================ package com.novoda.noplayer.internal.mediaplayer.forwarder; import android.media.MediaPlayer; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.PlayerState; import com.novoda.noplayer.internal.mediaplayer.CheckBufferHeartbeatCallback; public class MediaPlayerForwarder { private final MediaPlayerPreparedListener preparedListener; private final HeartBeatListener heartBeatListener; private final MediaPlayerCompletionListener completionListener; private final MediaPlayerErrorListener errorListener; private final VideoSizeChangedListener videoSizeChangedListener; public MediaPlayerForwarder() { preparedListener = new MediaPlayerPreparedListener(); heartBeatListener = new HeartBeatListener(); completionListener = new MediaPlayerCompletionListener(); errorListener = new MediaPlayerErrorListener(); videoSizeChangedListener = new VideoSizeChangedListener(); } public void bind(NoPlayer.PreparedListener preparedListener, PlayerState playerState) { this.preparedListener.add(new OnPreparedForwarder(preparedListener, playerState)); } public void bind(NoPlayer.BufferStateListener bufferStateListener, NoPlayer.ErrorListener errorListener) { preparedListener.add(new BufferOnPreparedListener(bufferStateListener)); heartBeatListener.add(new BufferHeartbeatListener(bufferStateListener)); this.errorListener.add(new ErrorForwarder(bufferStateListener, errorListener)); } public void bind(NoPlayer.CompletionListener completionListener, NoPlayer.StateChangedListener stateChangedListener) { this.completionListener.add(new CompletionForwarder(completionListener)); this.completionListener.add(new CompletionStateChangedForwarder(stateChangedListener)); } public void bind(NoPlayer.VideoSizeChangedListener videoSizeChangedListener) { this.videoSizeChangedListener.add(new VideoSizeChangedForwarder(videoSizeChangedListener)); } public void bind(NoPlayer.InfoListener infoListener) { preparedListener.add(new OnPreparedInfoForwarder(infoListener)); heartBeatListener.add(new BufferInfoForwarder(infoListener)); completionListener.add(new CompletionInfoForwarder(infoListener)); errorListener.add(new ErrorInfoForwarder(infoListener)); videoSizeChangedListener.add(new VideoSizeChangedInfoForwarder(infoListener)); } public MediaPlayer.OnPreparedListener onPreparedListener() { return preparedListener; } public CheckBufferHeartbeatCallback.BufferListener onHeartbeatListener() { return heartBeatListener; } public MediaPlayer.OnCompletionListener onCompletionListener() { return completionListener; } public MediaPlayer.OnErrorListener onErrorListener() { return errorListener; } public MediaPlayer.OnVideoSizeChangedListener onSizeChangedListener() { return videoSizeChangedListener; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/MediaPlayerPreparedListener.java ================================================ package com.novoda.noplayer.internal.mediaplayer.forwarder; import android.media.MediaPlayer; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; class MediaPlayerPreparedListener implements MediaPlayer.OnPreparedListener { private final List listeners = new CopyOnWriteArrayList<>(); void add(MediaPlayer.OnPreparedListener listener) { listeners.add(listener); } @Override public void onPrepared(MediaPlayer mp) { for (MediaPlayer.OnPreparedListener listener : listeners) { listener.onPrepared(mp); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/OnPreparedForwarder.java ================================================ package com.novoda.noplayer.internal.mediaplayer.forwarder; import android.media.MediaPlayer; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.PlayerState; class OnPreparedForwarder implements MediaPlayer.OnPreparedListener { private final NoPlayer.PreparedListener preparedListener; private final PlayerState playerState; OnPreparedForwarder(NoPlayer.PreparedListener preparedListener, PlayerState playerState) { this.preparedListener = preparedListener; this.playerState = playerState; } @Override public void onPrepared(MediaPlayer mp) { preparedListener.onPrepared(playerState); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/OnPreparedInfoForwarder.java ================================================ package com.novoda.noplayer.internal.mediaplayer.forwarder; import android.media.MediaPlayer; import com.novoda.noplayer.NoPlayer; import java.util.HashMap; class OnPreparedInfoForwarder implements MediaPlayer.OnPreparedListener { private final NoPlayer.InfoListener infoListener; OnPreparedInfoForwarder(NoPlayer.InfoListener infoListener) { this.infoListener = infoListener; } @Override public void onPrepared(MediaPlayer mp) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put("mp", String.valueOf(mp)); infoListener.onNewInfo("onPrepared", callingMethodParameters); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/VideoSizeChangedForwarder.java ================================================ package com.novoda.noplayer.internal.mediaplayer.forwarder; import android.media.MediaPlayer; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.internal.utils.NoPlayerLog; class VideoSizeChangedForwarder implements MediaPlayer.OnVideoSizeChangedListener { private final NoPlayer.VideoSizeChangedListener videoSizeChangedListener; private int previousWidth; private int previousHeight; VideoSizeChangedForwarder(NoPlayer.VideoSizeChangedListener videoSizeChangedListener) { this.videoSizeChangedListener = videoSizeChangedListener; } @Override public void onVideoSizeChanged(MediaPlayer mp, int width, int height) { if (bothDimensionsHaveChanged(width, height)) { videoSizeChangedListener.onVideoSizeChanged(width, height, 0, 1); } else { NoPlayerLog.w("Video size changed but we have swallowed the event due to only 1 dimension changing"); } previousWidth = width; previousHeight = height; } private boolean bothDimensionsHaveChanged(int width, int height) { boolean widthHasChanged = width != previousWidth; boolean heightHasChanged = height != previousHeight; return widthHasChanged && heightHasChanged; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/VideoSizeChangedInfoForwarder.java ================================================ package com.novoda.noplayer.internal.mediaplayer.forwarder; import android.media.MediaPlayer; import com.novoda.noplayer.NoPlayer; import java.util.HashMap; class VideoSizeChangedInfoForwarder implements MediaPlayer.OnVideoSizeChangedListener { private final NoPlayer.InfoListener infoListener; VideoSizeChangedInfoForwarder(NoPlayer.InfoListener infoListener) { this.infoListener = infoListener; } @Override public void onVideoSizeChanged(MediaPlayer mp, int width, int height) { HashMap callingMethodParameters = new HashMap<>(); callingMethodParameters.put("mp", String.valueOf(mp)); callingMethodParameters.put("width", String.valueOf(width)); callingMethodParameters.put("height", String.valueOf(height)); infoListener.onNewInfo("onVideoSizeChanged", callingMethodParameters); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/VideoSizeChangedListener.java ================================================ package com.novoda.noplayer.internal.mediaplayer.forwarder; import android.media.MediaPlayer; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; public class VideoSizeChangedListener implements MediaPlayer.OnVideoSizeChangedListener { private final List listeners = new CopyOnWriteArrayList<>(); public void add(MediaPlayer.OnVideoSizeChangedListener listener) { listeners.add(listener); } @Override public void onVideoSizeChanged(MediaPlayer mp, int width, int height) { for (MediaPlayer.OnVideoSizeChangedListener listener : listeners) { listener.onVideoSizeChanged(mp, width, height); } } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/utils/AndroidDeviceVersion.java ================================================ package com.novoda.noplayer.internal.utils; import android.os.Build; public class AndroidDeviceVersion { private final int sdkInt; public static AndroidDeviceVersion newInstance() { return new AndroidDeviceVersion(Build.VERSION.SDK_INT); } public AndroidDeviceVersion(int sdkInt) { this.sdkInt = sdkInt; } public boolean isJellyBeanEighteenOrAbove() { return sdkInt >= Build.VERSION_CODES.JELLY_BEAN_MR2; } public boolean isLollipopTwentyOneOrAbove() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; } public int sdkInt() { return sdkInt; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/utils/NoPlayerLog.java ================================================ package com.novoda.noplayer.internal.utils; import java.io.PrintWriter; import java.io.StringWriter; import java.util.Locale; @SuppressWarnings("PMD.ShortMethodName") // This is a logger class, the logging methods are 1-letter public final class NoPlayerLog { private static final String TAG = "No-Player"; private static final int DEPTH = 5; private static final int CLASS_SUFFIX = 5; private static final String DETAILED_LOG_TEMPLATE = "[%s][%s.%s:%d] %s"; private static boolean isEnabled = true; private NoPlayerLog() { // Not instantiable } public static void setLoggingEnabled(boolean enabled) { isEnabled = enabled; } private static String logMessage(String message, Throwable throwable) { StringBuilder detailedMessage = new StringBuilder(getDetailedLog(message)); if (throwable != null) { detailedMessage.append('\n').append(getStackTraceString(throwable)); } return detailedMessage.toString(); } private static String getDetailedLog(String message) { Thread current = Thread.currentThread(); final StackTraceElement trace = current.getStackTrace()[DEPTH]; final String filename = trace.getFileName(); return String.format(Locale.US, DETAILED_LOG_TEMPLATE, current.getName(), filename.substring(0, filename.length() - CLASS_SUFFIX), trace.getMethodName(), trace.getLineNumber(), message); } private static String getStackTraceString(Throwable throwable) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); try { throwable.printStackTrace(pw); return sw.toString().trim(); } finally { pw.close(); } } public static void d(String msg) { if (!isEnabled) { return; } android.util.Log.d(TAG, logMessage(msg, null)); } public static void d(Throwable throwable, String msg) { if (!isEnabled) { return; } android.util.Log.d(TAG, logMessage(msg, throwable)); } public static void e(String msg) { if (!isEnabled) { return; } android.util.Log.e(TAG, logMessage(msg, null)); } public static void e(Throwable throwable, String msg) { if (!isEnabled) { return; } android.util.Log.e(TAG, logMessage(msg, throwable)); } public static void i(String msg) { if (!isEnabled) { return; } android.util.Log.i(TAG, logMessage(msg, null)); } public static void i(Throwable throwable, String msg) { if (!isEnabled) { return; } android.util.Log.i(TAG, logMessage(msg, throwable)); } public static void v(String msg) { if (!isEnabled) { return; } android.util.Log.v(TAG, logMessage(msg, null)); } public static void v(Throwable throwable, String msg) { if (!isEnabled) { return; } android.util.Log.v(TAG, logMessage(msg, throwable)); } public static void w(String msg) { if (!isEnabled) { return; } android.util.Log.w(TAG, logMessage(msg, null)); } public static void w(Throwable throwable, String msg) { if (!isEnabled) { return; } android.util.Log.w(TAG, logMessage(msg, throwable)); } public static void wtf(String msg) { if (!isEnabled) { return; } android.util.Log.wtf(TAG, logMessage(msg, null)); } public static void wtf(Throwable throwable) { if (!isEnabled) { return; } android.util.Log.wtf(TAG, logMessage("", throwable)); } public static void wtf(Throwable throwable, String msg) { if (!isEnabled) { return; } android.util.Log.wtf(TAG, logMessage(msg, throwable)); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/internal/utils/Optional.java ================================================ package com.novoda.noplayer.internal.utils; public final class Optional { @SuppressWarnings("unchecked") // Type erasure has us covered here, we don't care private static final Optional ABSENT = new Optional(null); private final T data; @SuppressWarnings("unchecked") // Type erasure has us covered here, we don't care public static Optional absent() { return ABSENT; } public static Optional fromNullable(T data) { if (data == null) { return absent(); } return new Optional<>(data); } public static Optional of(T data) { if (data == null) { throw new IllegalArgumentException("Data cannot be null. Use Optional.fromNullable(maybeNullData)."); } return new Optional<>(data); } private Optional(T data) { this.data = data; } public boolean isPresent() { return data != null; } public boolean isAbsent() { return !isPresent(); } public T get() { if (!isPresent()) { throw new IllegalStateException("You must check if data is present before using get()"); } return data; } public T or(T elseCase) { return isPresent() ? get() : elseCase; } public Optional or(Optional elseCase) { return isPresent() ? this : elseCase; } public Optional or(Func0> elseFunc) { return isPresent() ? this : elseFunc.call(); } public interface Func0 { V call(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Optional optional = (Optional) o; return data != null ? data.equals(optional.data) : optional.data == null; } @Override public int hashCode() { return data != null ? data.hashCode() : 0; } @Override public String toString() { return String.format("Optional<%s>", isAbsent() ? "Absent" : data.toString()); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/model/AudioTracks.java ================================================ package com.novoda.noplayer.model; import java.util.Collections; import java.util.Iterator; import java.util.List; public final class AudioTracks implements Iterable { private final List playerAudioTracks; public static AudioTracks from(List audioTracks) { return new AudioTracks(Collections.unmodifiableList(audioTracks)); } private AudioTracks(List playerAudioTracks) { this.playerAudioTracks = playerAudioTracks; } public PlayerAudioTrack get(int index) { return playerAudioTracks.get(index); } public int size() { return playerAudioTracks.size(); } @Override public Iterator iterator() { return playerAudioTracks.iterator(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } AudioTracks that = (AudioTracks) o; return playerAudioTracks != null ? playerAudioTracks.equals(that.playerAudioTracks) : that.playerAudioTracks == null; } @Override public int hashCode() { return playerAudioTracks != null ? playerAudioTracks.hashCode() : 0; } @Override public String toString() { return "AudioTracks{playerAudioTracks=" + playerAudioTracks + '}'; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/model/Bitrate.java ================================================ package com.novoda.noplayer.model; public final class Bitrate { private static final int KILOBIT = 1000; private final long bitsPerSecond; public static Bitrate fromBitsPerSecond(long bitsPerSecond) { return new Bitrate(bitsPerSecond); } private Bitrate(long bitsPerSecond) { this.bitsPerSecond = bitsPerSecond; } public long asKilobits() { return bitsPerSecond / KILOBIT; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Bitrate bitrate = (Bitrate) o; return bitsPerSecond == bitrate.bitsPerSecond; } @Override public int hashCode() { return (int) (bitsPerSecond ^ (bitsPerSecond >>> 32)); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/model/Either.java ================================================ package com.novoda.noplayer.model; public abstract class Either { public static Either left(L left) { return new Left<>(left); } public static Either right(R right) { return new Right<>(right); } Either() { // restrict subclasses to the package } public abstract void apply(Consumer leftConsumer, Consumer rightConsumer); static class Left extends Either { private final L valueLeft; Left(L valueLeft) { this.valueLeft = valueLeft; } @Override public void apply(Consumer leftConsumer, Consumer rightConsumer) { leftConsumer.accept(valueLeft); } } static class Right extends Either { private final R valueRight; Right(R valueRight) { this.valueRight = valueRight; } @Override public void apply(Consumer leftConsumer, Consumer rightConsumer) { rightConsumer.accept(valueRight); } } public interface Consumer { void accept(T value); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/model/KeySetId.java ================================================ package com.novoda.noplayer.model; import java.util.Arrays; public final class KeySetId { private final byte[] keySetIdBytes; public static KeySetId of(byte[] sessionId) { return new KeySetId(Arrays.copyOf(sessionId, sessionId.length)); } @SuppressWarnings("PMD.ArrayIsStoredDirectly") // This array can only come from the factory method which does defensive copy private KeySetId(byte[] keySetIdBytes) { this.keySetIdBytes = keySetIdBytes; } public byte[] asBytes() { return Arrays.copyOf(keySetIdBytes, keySetIdBytes.length); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } KeySetId sessionId1 = (KeySetId) o; return Arrays.equals(keySetIdBytes, sessionId1.keySetIdBytes); } @Override public int hashCode() { return Arrays.hashCode(keySetIdBytes); } @Override public String toString() { return "KeySetId{keySetIdBytes=" + Arrays.toString(keySetIdBytes) + '}'; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/model/LoadTimeout.java ================================================ package com.novoda.noplayer.model; import android.os.Handler; import com.novoda.noplayer.internal.Clock; import com.novoda.noplayer.NoPlayer; public class LoadTimeout { private static final int DELAY_MILLIS = 1000; private final Clock clock; private final Handler handler; private long startTime; private long endTime; private NoPlayer.LoadTimeoutCallback loadTimeoutCallback; public LoadTimeout(Clock clock, Handler handler) { this.clock = clock; this.handler = handler; } public void start(Timeout timeout, NoPlayer.LoadTimeoutCallback loadTimeoutCallback) { cancel(); this.loadTimeoutCallback = loadTimeoutCallback; startTime = clock.getCurrentTime(); endTime = startTime + timeout.inMillis(); handler.post(loadTimeoutCheck); } private final Runnable loadTimeoutCheck = new Runnable() { @Override public void run() { if (clock.getCurrentTime() >= endTime) { loadTimeoutCallback.onLoadTimeout(); cancel(); } else { handler.postDelayed(this, DELAY_MILLIS); } } }; public void cancel() { startTime = 0; loadTimeoutCallback = NoPlayer.LoadTimeoutCallback.NULL_IMPL; handler.removeCallbacks(loadTimeoutCheck); } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/model/NoPlayerCue.java ================================================ package com.novoda.noplayer.model; import android.graphics.Bitmap; import android.text.Layout.Alignment; public class NoPlayerCue { private final CharSequence text; private final Alignment textAlignment; private final Bitmap bitmap; private final float line; private final int lineType; private final int lineAnchor; private final float position; private final int positionAnchor; private final float size; private final float bitmapHeight; private final boolean windowColorSet; private final int windowColor; @SuppressWarnings({"checkstyle:ParameterNumber", "PMD.ExcessiveParameterList"}) // TODO group parameters into classes public NoPlayerCue(CharSequence text, Alignment textAlignment, Bitmap bitmap, float line, int lineType, int lineAnchor, float position, int positionAnchor, float size, float bitmapHeight, boolean windowColorSet, int windowColor) { this.text = text; this.textAlignment = textAlignment; this.bitmap = bitmap; this.line = line; this.lineType = lineType; this.lineAnchor = lineAnchor; this.position = position; this.positionAnchor = positionAnchor; this.size = size; this.bitmapHeight = bitmapHeight; this.windowColorSet = windowColorSet; this.windowColor = windowColor; } public CharSequence text() { return text; } public Alignment textAlignment() { return textAlignment; } public Bitmap bitmap() { return bitmap; } public float line() { return line; } public int lineType() { return lineType; } public int lineAnchor() { return lineAnchor; } public float position() { return position; } public int positionAnchor() { return positionAnchor; } public float size() { return size; } public float bitmapHeight() { return bitmapHeight; } public boolean windowColorSet() { return windowColorSet; } public int windowColor() { return windowColor; } @Override public String toString() { return "NoPlayerCue{" + "text=" + text + ", textAlignment=" + textAlignment + ", bitmap=" + bitmap + ", line=" + line + ", lineType=" + lineType + ", lineAnchor=" + lineAnchor + ", position=" + position + ", positionAnchor=" + positionAnchor + ", size=" + size + ", bitmapHeight=" + bitmapHeight + ", windowColorSet=" + windowColorSet + ", windowColor=" + windowColor + '}'; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } NoPlayerCue that = (NoPlayerCue) o; if (Float.compare(that.line, line) != 0) { return false; } if (lineType != that.lineType) { return false; } if (lineAnchor != that.lineAnchor) { return false; } if (Float.compare(that.position, position) != 0) { return false; } if (positionAnchor != that.positionAnchor) { return false; } if (Float.compare(that.size, size) != 0) { return false; } if (Float.compare(that.bitmapHeight, bitmapHeight) != 0) { return false; } if (windowColorSet != that.windowColorSet) { return false; } if (windowColor != that.windowColor) { return false; } if (text != null ? !text.equals(that.text) : that.text != null) { return false; } if (textAlignment != that.textAlignment) { return false; } return bitmap != null ? bitmap.equals(that.bitmap) : that.bitmap == null; } @Override public int hashCode() { int result = text != null ? text.hashCode() : 0; result = 31 * result + (textAlignment != null ? textAlignment.hashCode() : 0); result = 31 * result + (bitmap != null ? bitmap.hashCode() : 0); result = 31 * result + (line != +0.0f ? Float.floatToIntBits(line) : 0); result = 31 * result + lineType; result = 31 * result + lineAnchor; result = 31 * result + (position != +0.0f ? Float.floatToIntBits(position) : 0); result = 31 * result + positionAnchor; result = 31 * result + (size != +0.0f ? Float.floatToIntBits(size) : 0); result = 31 * result + (bitmapHeight != +0.0f ? Float.floatToIntBits(bitmapHeight) : 0); result = 31 * result + (windowColorSet ? 1 : 0); result = 31 * result + windowColor; return result; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/model/PlayerAudioTrack.java ================================================ package com.novoda.noplayer.model; import com.novoda.noplayer.internal.exoplayer.mediasource.AudioTrackType; public class PlayerAudioTrack { private final int groupIndex; private final int formatIndex; private final String trackId; private final String language; private final String mimeType; private final int numberOfChannels; private final int frequency; private final AudioTrackType audioTrackType; @SuppressWarnings("checkstyle:ParameterNumber") // TODO group parameters into classes public PlayerAudioTrack(int groupIndex, int formatIndex, String trackId, String language, String mimeType, int numberOfChannels, int frequency, AudioTrackType audioTrackType) { this.groupIndex = groupIndex; this.formatIndex = formatIndex; this.trackId = trackId; this.language = language; this.mimeType = mimeType; this.numberOfChannels = numberOfChannels; this.frequency = frequency; this.audioTrackType = audioTrackType; } public int groupIndex() { return groupIndex; } public int formatIndex() { return formatIndex; } public String trackId() { return trackId; } public String language() { return language; } public String mimeType() { return mimeType; } public int numberOfChannels() { return numberOfChannels; } public int frequency() { return frequency; } public AudioTrackType audioTrackType() { return audioTrackType; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } PlayerAudioTrack that = (PlayerAudioTrack) o; if (groupIndex != that.groupIndex) { return false; } if (formatIndex != that.formatIndex) { return false; } if (numberOfChannels != that.numberOfChannels) { return false; } if (frequency != that.frequency) { return false; } if (trackId != null ? !trackId.equals(that.trackId) : that.trackId != null) { return false; } if (language != null ? !language.equals(that.language) : that.language != null) { return false; } if (mimeType != null ? !mimeType.equals(that.mimeType) : that.mimeType != null) { return false; } return audioTrackType == that.audioTrackType; } @Override public int hashCode() { int result = groupIndex; result = 31 * result + formatIndex; result = 31 * result + (trackId != null ? trackId.hashCode() : 0); result = 31 * result + (language != null ? language.hashCode() : 0); result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0); result = 31 * result + numberOfChannels; result = 31 * result + frequency; result = 31 * result + (audioTrackType != null ? audioTrackType.hashCode() : 0); return result; } @Override public String toString() { return "PlayerAudioTrack{" + "groupIndex=" + groupIndex + ", formatIndex=" + formatIndex + ", trackId='" + trackId + '\'' + ", language='" + language + '\'' + ", mimeType='" + mimeType + '\'' + ", numberOfChannels=" + numberOfChannels + ", frequency=" + frequency + ", audioTrackType=" + audioTrackType + '}'; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/model/PlayerSubtitleTrack.java ================================================ package com.novoda.noplayer.model; public class PlayerSubtitleTrack { private final int groupIndex; private final int formatIndex; private final String trackId; private final String language; private final String mimeType; private final int numberOfChannels; private final int frequency; public PlayerSubtitleTrack(int groupIndex, int formatIndex, String trackId, String language, String mimeType, int numberOfChannels, int frequency) { this.groupIndex = groupIndex; this.formatIndex = formatIndex; this.trackId = trackId; this.language = language; this.mimeType = mimeType; this.numberOfChannels = numberOfChannels; this.frequency = frequency; } public int groupIndex() { return groupIndex; } public int formatIndex() { return formatIndex; } public String trackId() { return trackId; } public String language() { return language; } public String mimeType() { return mimeType; } public int numberOfChannels() { return numberOfChannels; } public int frequency() { return frequency; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof PlayerSubtitleTrack)) { return false; } PlayerSubtitleTrack that = (PlayerSubtitleTrack) o; if (groupIndex != that.groupIndex) { return false; } if (formatIndex != that.formatIndex) { return false; } if (numberOfChannels != that.numberOfChannels) { return false; } if (frequency != that.frequency) { return false; } if (trackId != null ? !trackId.equals(that.trackId) : that.trackId != null) { return false; } if (language != null ? !language.equals(that.language) : that.language != null) { return false; } return mimeType != null ? mimeType.equals(that.mimeType) : that.mimeType == null; } @Override public int hashCode() { int result = groupIndex; result = 31 * result + formatIndex; result = 31 * result + (trackId != null ? trackId.hashCode() : 0); result = 31 * result + (language != null ? language.hashCode() : 0); result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0); result = 31 * result + numberOfChannels; result = 31 * result + frequency; return result; } @Override public String toString() { return "PlayerSubtitleTrack{" + "groupIndex=" + groupIndex + ", formatIndex=" + formatIndex + ", trackId='" + trackId + '\'' + ", language='" + language + '\'' + ", mimeType='" + mimeType + '\'' + ", numberOfChannels=" + numberOfChannels + ", frequency=" + frequency + '}'; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/model/PlayerVideoTrack.java ================================================ package com.novoda.noplayer.model; import com.novoda.noplayer.ContentType; public class PlayerVideoTrack { private final int groupIndex; private final int formatIndex; private final String id; private final ContentType contentType; private final int width; private final int height; private final int fps; private final int bitrate; @SuppressWarnings("checkstyle:ParameterNumber") // TODO group parameters into classes public PlayerVideoTrack(int groupIndex, int formatIndex, String id, ContentType contentType, int width, int height, int fps, int bitrate) { this.groupIndex = groupIndex; this.formatIndex = formatIndex; this.id = id; this.contentType = contentType; this.width = width; this.height = height; this.fps = fps; this.bitrate = bitrate; } public int groupIndex() { return groupIndex; } public int formatIndex() { return formatIndex; } public String id() { return id; } public ContentType contentType() { return contentType; } public int width() { return width; } public int height() { return height; } public int fps() { return fps; } public int bitrate() { return bitrate; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } PlayerVideoTrack that = (PlayerVideoTrack) o; if (groupIndex != that.groupIndex) { return false; } if (formatIndex != that.formatIndex) { return false; } if (width != that.width) { return false; } if (height != that.height) { return false; } if (fps != that.fps) { return false; } if (bitrate != that.bitrate) { return false; } if (id != null ? !id.equals(that.id) : that.id != null) { return false; } return contentType == that.contentType; } @Override public int hashCode() { int result = groupIndex; result = 31 * result + formatIndex; result = 31 * result + (id != null ? id.hashCode() : 0); result = 31 * result + (contentType != null ? contentType.hashCode() : 0); result = 31 * result + width; result = 31 * result + height; result = 31 * result + fps; result = 31 * result + bitrate; return result; } @Override public String toString() { return "PlayerVideoTrack{" + "groupIndex=" + groupIndex + ", formatIndex=" + formatIndex + ", id='" + id + '\'' + ", contentType=" + contentType + ", width=" + width + ", height=" + height + ", fps=" + fps + ", bitrate=" + bitrate + '}'; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/model/TextCues.java ================================================ package com.novoda.noplayer.model; import java.util.Collections; import java.util.List; public final class TextCues { private final List cues; public static TextCues of(List cues) { return new TextCues(Collections.unmodifiableList(cues)); } private TextCues(List cues) { this.cues = cues; } public int size() { return cues.size(); } public boolean isEmpty() { return cues.isEmpty(); } public NoPlayerCue get(int position) { return cues.get(position); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } TextCues textCues = (TextCues) o; return cues != null ? cues.equals(textCues.cues) : textCues.cues == null; } @Override public int hashCode() { return cues != null ? cues.hashCode() : 0; } @Override public String toString() { return "TextCues{" + "cues=" + cues + '}'; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/model/Timeout.java ================================================ package com.novoda.noplayer.model; import java.util.concurrent.TimeUnit; public final class Timeout { private static final long ONE_SECOND_IN_MILLIS = TimeUnit.SECONDS.toMillis(1); private final long timeoutInMillis; public static Timeout fromSeconds(long timeoutInSeconds) { return new Timeout(timeoutInSeconds * ONE_SECOND_IN_MILLIS); } private Timeout(long timeoutInMillis) { this.timeoutInMillis = timeoutInMillis; } public long inMillis() { return timeoutInMillis; } } ================================================ FILE: core/src/main/java/com/novoda/noplayer/text/NoPlayerSubtitleDecoderFactory.java ================================================ package com.novoda.noplayer.text; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.text.SubtitleDecoder; import com.google.android.exoplayer2.text.SubtitleDecoderFactory; import com.google.android.exoplayer2.text.cea.Cea608Decoder; import com.google.android.exoplayer2.text.cea.Cea708Decoder; import com.google.android.exoplayer2.text.dvb.DvbDecoder; import com.google.android.exoplayer2.text.pgs.PgsDecoder; import com.google.android.exoplayer2.text.ssa.SsaDecoder; import com.google.android.exoplayer2.text.subrip.SubripDecoder; import com.google.android.exoplayer2.text.ttml.TtmlDecoder; import com.google.android.exoplayer2.text.tx3g.Tx3gDecoder; import com.google.android.exoplayer2.text.webvtt.Mp4WebvttDecoder; import com.google.android.exoplayer2.util.MimeTypes; import com.novoda.noplayer.external.exoplayer.text.webvtt.WebvttDecoder; // This is a factory and we need to consider all the supported formats when creating a decoder @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.StdCyclomaticComplexity"}) public class NoPlayerSubtitleDecoderFactory implements SubtitleDecoderFactory { @Override public boolean supportsFormat(Format format) { String mimeType = format.sampleMimeType; return MimeTypes.TEXT_VTT.equals(mimeType) || MimeTypes.TEXT_SSA.equals(mimeType) || MimeTypes.APPLICATION_TTML.equals(mimeType) || MimeTypes.APPLICATION_MP4VTT.equals(mimeType) || MimeTypes.APPLICATION_SUBRIP.equals(mimeType) || MimeTypes.APPLICATION_TX3G.equals(mimeType) || MimeTypes.APPLICATION_CEA608.equals(mimeType) || MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) || MimeTypes.APPLICATION_CEA708.equals(mimeType) || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType) || MimeTypes.APPLICATION_PGS.equals(mimeType); } @Override public SubtitleDecoder createDecoder(Format format) { switch (format.sampleMimeType) { case MimeTypes.TEXT_VTT: return new WebvttDecoder(); case MimeTypes.TEXT_SSA: return new SsaDecoder(format.initializationData); case MimeTypes.APPLICATION_MP4VTT: return new Mp4WebvttDecoder(); case MimeTypes.APPLICATION_TTML: return new TtmlDecoder(); case MimeTypes.APPLICATION_SUBRIP: return new SubripDecoder(); case MimeTypes.APPLICATION_TX3G: return new Tx3gDecoder(format.initializationData); case MimeTypes.APPLICATION_CEA608: case MimeTypes.APPLICATION_MP4CEA608: return new Cea608Decoder(format.sampleMimeType, format.accessibilityChannel); case MimeTypes.APPLICATION_CEA708: return new Cea708Decoder(format.accessibilityChannel, format.initializationData); case MimeTypes.APPLICATION_DVBSUBS: return new DvbDecoder(format.initializationData); case MimeTypes.APPLICATION_PGS: return new PgsDecoder(); default: throw new IllegalArgumentException("Attempted to create decoder for unsupported format"); } } } ================================================ FILE: core/src/main/res/layout/noplayer_view.xml ================================================ ================================================ FILE: core/src/test/java/com/google/android/exoplayer2/ExoPlaybackExceptionFactory.java ================================================ package com.google.android.exoplayer2; public class ExoPlaybackExceptionFactory { public static ExoPlaybackException createForUnexpected(RuntimeException exception) { return ExoPlaybackException.createForUnexpected(exception); } } ================================================ FILE: core/src/test/java/com/google/android/exoplayer2/drm/FrameworkMediaCryptoFixture.java ================================================ package com.google.android.exoplayer2.drm; import android.media.MediaCrypto; import android.media.MediaCryptoException; import java.util.UUID; public final class FrameworkMediaCryptoFixture { private MediaCrypto mediaCrypto = new MediaCrypto(UUID.randomUUID(), new byte[0]); private boolean forceAllowInsecureDecoderComponents = true; private FrameworkMediaCryptoFixture() throws MediaCryptoException { // Static factory method. } public static FrameworkMediaCryptoFixture aFrameworkMediaCrypto() throws MediaCryptoException { return new FrameworkMediaCryptoFixture(); } public FrameworkMediaCryptoFixture withMediaCrypto(MediaCrypto mediaCrypto) { this.mediaCrypto = mediaCrypto; return this; } public FrameworkMediaCryptoFixture withForceAllowInsecureDecoderComponents(boolean forceAllowInsecureDecoderComponents) { this.forceAllowInsecureDecoderComponents = forceAllowInsecureDecoderComponents; return this; } public FrameworkMediaCrypto build() { return new FrameworkMediaCrypto(mediaCrypto, forceAllowInsecureDecoderComponents); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/LoadTimeoutTest.java ================================================ package com.novoda.noplayer; import android.os.Handler; import com.novoda.noplayer.internal.Clock; import com.novoda.noplayer.model.LoadTimeout; import com.novoda.noplayer.model.Timeout; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.mockito.stubbing.Answer; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class LoadTimeoutTest { private static final long START_TIME = 0L; private static final long END_TIME = 1000L; private static final int RESCHEDULE_DELAY_MILLIS = 1000; private static final Timeout TIMEOUT_NOT_REACHED = Timeout.fromSeconds(5); private static final Timeout TIMEOUT_REACHED = Timeout.fromSeconds(1); @Rule public MockitoRule rule = MockitoJUnit.rule(); @Mock Clock clock; @Mock Handler handler; @Mock NoPlayer.LoadTimeoutCallback loadTimeoutCallback; private LoadTimeout loadTimeout; @Before public void setUp() { loadTimeout = new LoadTimeout(clock, handler); doAnswer(new Answer() { @Override public Void answer(InvocationOnMock invocation) throws Throwable { Runnable runnable = invocation.getArgument(0); runnable.run(); return null; } }).when(handler).post(any(Runnable.class)); } @Test public void givenTimeoutIsReached_whenStarting_thenOnLoadTimeoutIsCalled() { when(clock.getCurrentTime()).thenReturn(START_TIME, END_TIME); loadTimeout.start(TIMEOUT_REACHED, loadTimeoutCallback); verify(loadTimeoutCallback).onLoadTimeout(); } @Test public void givenTimeoutIsNotReached_whenStarting_thenTimeoutIsRescheduled() { when(clock.getCurrentTime()).thenReturn(START_TIME, END_TIME); loadTimeout.start(TIMEOUT_NOT_REACHED, loadTimeoutCallback); ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); verify(handler).post(captor.capture()); verify(handler).postDelayed(captor.getValue(), RESCHEDULE_DELAY_MILLIS); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/NoPlayerCreatorTest.java ================================================ package com.novoda.noplayer; import android.content.Context; import com.novoda.noplayer.drm.DownloadedModularDrm; import com.novoda.noplayer.drm.DrmHandler; import com.novoda.noplayer.drm.DrmType; import com.novoda.noplayer.drm.StreamingModularDrm; import com.novoda.noplayer.internal.exoplayer.NoPlayerExoPlayerCreator; import com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreator; import com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreatorException; import com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreatorFactory; import com.novoda.noplayer.internal.mediaplayer.NoPlayerMediaPlayerCreator; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.experimental.runners.Enclosed; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import java.util.Arrays; import java.util.List; import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @RunWith(Enclosed.class) public class NoPlayerCreatorTest { public abstract static class Base { static final boolean USE_SECURE_CODEC = false; static final boolean ALLOW_CROSS_PROTOCOL_REDIRECTS = false; static final StreamingModularDrm STREAMING_MODULAR_DRM = mock(StreamingModularDrm.class); static final DownloadedModularDrm DOWNLOADED_MODULAR_DRM = mock(DownloadedModularDrm.class); static final NoPlayer EXO_PLAYER = mock(NoPlayer.class); static final NoPlayer MEDIA_PLAYER = mock(NoPlayer.class); @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock Context context; @Mock NoPlayerExoPlayerCreator noPlayerExoPlayerCreator; @Mock NoPlayerMediaPlayerCreator noPlayerMediaPlayerCreator; @Mock DrmSessionCreator drmSessionCreator; @Mock DrmSessionCreatorFactory drmSessionCreatorFactory; NoPlayerCreator noPlayerCreator; @Before public void setUp() throws DrmSessionCreatorException { given(drmSessionCreatorFactory.createFor(any(DrmType.class), any(DrmHandler.class))).willReturn(drmSessionCreator); given(noPlayerExoPlayerCreator.createExoPlayer(context, drmSessionCreator, USE_SECURE_CODEC, ALLOW_CROSS_PROTOCOL_REDIRECTS)).willReturn(EXO_PLAYER); given(noPlayerMediaPlayerCreator.createMediaPlayer(context)).willReturn(MEDIA_PLAYER); noPlayerCreator = new NoPlayerCreator(context, prioritizedPlayerTypes(), noPlayerExoPlayerCreator, noPlayerMediaPlayerCreator, drmSessionCreatorFactory); } abstract List prioritizedPlayerTypes(); } public static class GivenMediaPlayerPrioritized extends Base { @Override List prioritizedPlayerTypes() { return Arrays.asList(PlayerType.MEDIA_PLAYER, PlayerType.EXO_PLAYER); } @Test public void whenCreatingPlayerWithDrmTypeNone_thenReturnsMediaPlayer() { NoPlayer player = noPlayerCreator.create(DrmType.NONE, DrmHandler.NO_DRM, USE_SECURE_CODEC, ALLOW_CROSS_PROTOCOL_REDIRECTS); assertThat(player).isEqualTo(MEDIA_PLAYER); } @Test public void whenCreatingPlayerWithDrmTypeWidevineClassic_thenReturnsMediaPlayer() { NoPlayer player = noPlayerCreator.create(DrmType.WIDEVINE_CLASSIC, DrmHandler.NO_DRM, USE_SECURE_CODEC, ALLOW_CROSS_PROTOCOL_REDIRECTS); assertThat(player).isEqualTo(MEDIA_PLAYER); } @Test public void whenCreatingPlayerWithDrmTypeWidevineModularStream_thenReturnsExoPlayer() { NoPlayer player = noPlayerCreator.create(DrmType.WIDEVINE_MODULAR_STREAM, STREAMING_MODULAR_DRM, USE_SECURE_CODEC, ALLOW_CROSS_PROTOCOL_REDIRECTS); assertThat(player).isEqualTo(EXO_PLAYER); } @Test public void whenCreatingPlayerWithDrmTypeWidevineModularDownload_thenReturnsExoPlayer() { NoPlayer player = noPlayerCreator.create(DrmType.WIDEVINE_MODULAR_DOWNLOAD, DOWNLOADED_MODULAR_DRM, USE_SECURE_CODEC, ALLOW_CROSS_PROTOCOL_REDIRECTS); assertThat(player).isEqualTo(EXO_PLAYER); } } public static class GivenExoPlayerPlayerPrioritized extends Base { @Override List prioritizedPlayerTypes() { return Arrays.asList(PlayerType.EXO_PLAYER, PlayerType.MEDIA_PLAYER); } @Test public void whenCreatingPlayerWithDrmTypeNone_thenReturnsExoPlayer() { NoPlayer player = noPlayerCreator.create(DrmType.NONE, DrmHandler.NO_DRM, USE_SECURE_CODEC, ALLOW_CROSS_PROTOCOL_REDIRECTS); assertThat(player).isEqualTo(EXO_PLAYER); } @Test public void whenCreatingPlayerWithDrmTypeWidevineClassic_thenReturnsMediaPlayer() { NoPlayer player = noPlayerCreator.create(DrmType.WIDEVINE_CLASSIC, DrmHandler.NO_DRM, USE_SECURE_CODEC, ALLOW_CROSS_PROTOCOL_REDIRECTS); assertThat(player).isEqualTo(MEDIA_PLAYER); } @Test public void whenCreatingPlayerWithDrmTypeWidevineModularStream_thenReturnsExoPlayer() { NoPlayer player = noPlayerCreator.create(DrmType.WIDEVINE_MODULAR_STREAM, STREAMING_MODULAR_DRM, USE_SECURE_CODEC, ALLOW_CROSS_PROTOCOL_REDIRECTS); assertThat(player).isEqualTo(EXO_PLAYER); } @Test public void whenCreatingPlayerWithDrmTypeWidevineModularDownload_thenReturnsExoPlayer() { NoPlayer player = noPlayerCreator.create(DrmType.WIDEVINE_MODULAR_DOWNLOAD, DOWNLOADED_MODULAR_DRM, USE_SECURE_CODEC, ALLOW_CROSS_PROTOCOL_REDIRECTS); assertThat(player).isEqualTo(EXO_PLAYER); } } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/PlayerSurfaceHolderTest.java ================================================ package com.novoda.noplayer; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.TextureView; import com.google.android.exoplayer2.Player; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) public class PlayerSurfaceHolderTest { @Mock private SurfaceView surfaceView; @Mock private SurfaceHolder surfaceHolder; @Mock private TextureView textureView; @Mock private Player.VideoComponent videoPlayer; @Rule public ExpectedException thrown = ExpectedException.none(); @Before public void setUp() { surfaceHolder = mock(SurfaceHolder.class); given(surfaceView.getHolder()).willReturn(surfaceHolder); } @Test public void whenCreatingPlayerSurfaceHolderWithSurfaceView_thenAttachCallbackToSurfaceHolder() { PlayerSurfaceHolder.create(surfaceView); verify(surfaceHolder).addCallback(any(PlayerViewSurfaceHolder.class)); } @Test public void whenCreatingPlayerSurfaceHolderWithTextureView_thenAttachSurfaceTextureListenerToTextureView() { PlayerSurfaceHolder.create(textureView); verify(textureView).setSurfaceTextureListener(any(PlayerViewSurfaceHolder.class)); } @Test public void givenPlayerSurfaceHolderContainsSurfaceView_whenAttachingVideoPlayer_thenSetsVideoSurfaceView() { PlayerSurfaceHolder playerSurfaceHolder = PlayerSurfaceHolder.create(surfaceView); playerSurfaceHolder.attach(videoPlayer); verify(videoPlayer).setVideoSurfaceView(surfaceView); } @Test public void givenPlayerSurfaceHolderContainsTextureView_whenAttachingVideoPlayer_thenSetsVideoTextureView() { PlayerSurfaceHolder playerSurfaceHolder = PlayerSurfaceHolder.create(textureView); playerSurfaceHolder.attach(videoPlayer); verify(videoPlayer).setVideoTextureView(textureView); } @Test public void givenPlayerSurfaceHolderContainsNoView_whenAttachingVideoPlayer_thenThrowsException() { thrown.expect(IllegalArgumentException.class); PlayerSurfaceHolder playerSurfaceHolder = new PlayerSurfaceHolder(null, null, null); playerSurfaceHolder.attach(videoPlayer); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/PlayerTypeTest.java ================================================ package com.novoda.noplayer; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; public class PlayerTypeTest { @Rule public ExpectedException thrown = ExpectedException.none(); @Test public void givenUnknownPlayerType_thenThrows() throws Exception { thrown.expect(PlayerType.UnknownPlayerTypeException.class); PlayerType.from("unknown player type 1234___"); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/HeartTest.java ================================================ package com.novoda.noplayer.internal; import android.os.Handler; import com.novoda.noplayer.NoPlayer; import org.junit.Before; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.BDDMockito.will; import static org.mockito.Mockito.mock; public class HeartTest { private final NoPlayer.HeartbeatCallback heartbeatCallback = mock(NoPlayer.HeartbeatCallback.class); private final NoPlayer noPlayer = mock(NoPlayer.class); private final Handler handler = mock(Handler.class); private Heart heart; @Before public void setUp() { will(new Answer() { @Override public Void answer(InvocationOnMock invocation) { Runnable runnable = invocation.getArgument(0); runnable.run(); return null; } }).given(handler).post(any(Runnable.class)); heart = Heart.newInstance(handler); } @Test(expected = IllegalStateException.class) public void throwsException_whenStartingHeartWithoutBindingAction() { heart.startBeatingHeart(); } @Test(expected = IllegalStateException.class) public void throwsException_whenForcingBeatWithoutBindingAction() { heart.forceBeat(); } @Test public void removesCallbacks_whenStartingHeart() { Heart.Heartbeat onHeartbeat = new Heart.Heartbeat(heartbeatCallback, noPlayer); heart.bind(onHeartbeat); heart.startBeatingHeart(); then(handler).should().removeCallbacks(any(Runnable.class)); } @Test public void schedulesNextBeat_whenStartingHeart() { Heart.Heartbeat onHeartbeat = new Heart.Heartbeat(heartbeatCallback, noPlayer); heart.bind(onHeartbeat); heart.startBeatingHeart(); then(handler).should().postDelayed(any(Runnable.class), anyLong()); } @Test public void doesNotEmitOnBeat_whenPlayerIsNotPlaying() { Heart.Heartbeat onHeartbeat = new Heart.Heartbeat(heartbeatCallback, noPlayer); heart.bind(onHeartbeat); heart.startBeatingHeart(); then(heartbeatCallback).shouldHaveZeroInteractions(); } @Test public void emitsOnBeat_whenPlayerIsPlaying() { given(noPlayer.isPlaying()).willReturn(true); Heart.Heartbeat onHeartbeat = new Heart.Heartbeat(heartbeatCallback, noPlayer); heart.bind(onHeartbeat); heart.startBeatingHeart(); then(heartbeatCallback).should().onBeat(noPlayer); } @Test public void emitsOnBeat_whenForcingBeat() { given(noPlayer.isPlaying()).willReturn(true); Heart.Heartbeat onHeartbeat = new Heart.Heartbeat(heartbeatCallback, noPlayer); heart.bind(onHeartbeat); heart.forceBeat(); then(heartbeatCallback).should().onBeat(noPlayer); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/drm/provision/HttpPostingProvisionExecutorTest.java ================================================ package com.novoda.noplayer.internal.drm.provision; import com.novoda.noplayer.drm.ModularDrmProvisionRequest; import java.io.IOException; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import static com.novoda.noplayer.internal.drm.provision.ProvisioningCapabilitiesFixtures.aProvisioningCapabilities; import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.Mockito.verify; public class HttpPostingProvisionExecutorTest { private static final String PROVISION_URL = "http://provisionurl.com"; private static final byte[] PROVISION_DATA = "provision-payload".getBytes(); private static final ModularDrmProvisionRequest A_PROVISION_REQUEST = new ModularDrmProvisionRequest(PROVISION_URL, PROVISION_DATA); @Rule public MockitoRule rule = MockitoJUnit.rule(); private HttpPostingProvisionExecutor httpPostingProvisionExecutor; private ArgumentCaptor provisionUrlCaptor; @Mock private HttpUrlConnectionPoster httpPoster; @Before public void setUp() { provisionUrlCaptor = ArgumentCaptor.forClass(String.class); } @Test(expected = UnableToProvisionException.class) public void givenNonCapableProvisionCapabilities_whenProvisioning_thenAnUnableToProvisionExceptionIsThrown() throws IOException, UnableToProvisionException { ProvisioningCapabilities capabilities = aProvisioningCapabilities().thatCannotProvision(); httpPostingProvisionExecutor = new HttpPostingProvisionExecutor(httpPoster, capabilities); httpPostingProvisionExecutor.execute(A_PROVISION_REQUEST); } @Test public void givenCapableProvisionCapabilities_whenProvisioning_thenTheRequestUrlIsExpected() throws IOException, UnableToProvisionException { ProvisioningCapabilities capabilities = aProvisioningCapabilities().thatCanProvision(); httpPostingProvisionExecutor = new HttpPostingProvisionExecutor(httpPoster, capabilities); String expectedProvisionUrl = PROVISION_URL + "&signedRequest=" + new String(PROVISION_DATA); httpPostingProvisionExecutor.execute(A_PROVISION_REQUEST); verify(httpPoster).post(provisionUrlCaptor.capture()); assertThat(provisionUrlCaptor.getValue()).isEqualTo(expectedProvisionUrl); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/drm/provision/ProvisioningCapabilitiesFixtures.java ================================================ package com.novoda.noplayer.internal.drm.provision; import android.os.Build; public final class ProvisioningCapabilitiesFixtures { public static ProvisioningCapabilitiesFixtures aProvisioningCapabilities() { return new ProvisioningCapabilitiesFixtures(); } private ProvisioningCapabilitiesFixtures() { // Not instantiable } public ProvisioningCapabilities thatCanProvision() { return new ProvisioningCapabilities(Build.VERSION_CODES.JELLY_BEAN_MR2); } public ProvisioningCapabilities thatCannotProvision() { return new ProvisioningCapabilities(Build.VERSION_CODES.JELLY_BEAN_MR1); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerFacadeTest.java ================================================ package com.novoda.noplayer.internal.exoplayer; import android.net.Uri; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.TextureView; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.novoda.noplayer.ContentType; import com.novoda.noplayer.Options; import com.novoda.noplayer.OptionsBuilder; import com.novoda.noplayer.PlayerSurfaceHolder; import com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreator; import com.novoda.noplayer.internal.exoplayer.forwarder.ExoPlayerForwarder; import com.novoda.noplayer.internal.exoplayer.mediasource.MediaSourceFactory; import com.novoda.noplayer.internal.utils.AndroidDeviceVersion; import com.novoda.noplayer.internal.utils.Optional; import com.novoda.noplayer.model.AudioTracks; import com.novoda.noplayer.model.PlayerAudioTrack; import com.novoda.noplayer.model.PlayerAudioTrackFixture; import com.novoda.noplayer.model.PlayerSubtitleTrack; import com.novoda.noplayer.model.PlayerVideoTrack; import com.novoda.noplayer.model.PlayerVideoTrackFixture; import java.util.Collections; import java.util.List; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.experimental.runners.Enclosed; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import utils.ExceptionMatcher; import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willDoNothing; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @RunWith(Enclosed.class) public class ExoPlayerFacadeTest { private static final boolean SELECTED = true; private static final long TWENTY_FIVE_SECONDS_IN_MILLIS = 25000; private static final long TWO_MINUTES_IN_MILLIS = 120000; private static final long TEN_MINUTES_IN_MILLIS = 600000; private static final int TEN_PERCENT = 10; private static final boolean IS_PLAYING = true; private static final boolean PLAY_WHEN_READY = true; private static final boolean DO_NOT_PLAY_WHEN_READY = false; private static final boolean RESET_POSITION = true; private static final boolean DO_NOT_RESET_POSITION = false; private static final boolean DO_NOT_RESET_STATE = false; private static final Options OPTIONS = new OptionsBuilder() .withContentType(ContentType.DASH) .build(); public static class GivenVideoNotLoaded extends Base { private static final long ANY_POSITION = 1000; private static final PlayerAudioTrack PLAYER_AUDIO_TRACK = PlayerAudioTrackFixture.aPlayerAudioTrack().build(); private static final AudioTracks AUDIO_TRACKS = AudioTracks.from(Collections.singletonList(PLAYER_AUDIO_TRACK)); @Rule public ExpectedException thrown = ExpectedException.none(); @Test public void whenResetting_thenReleasesUnderlyingPlayer() { facade.release(); verify(exoPlayer, never()).release(); } @Test public void whenLoadingVideo_thenAddsPlayerEventListener() { facade.loadVideo(surfaceViewHolder, drmSessionCreator, uri, OPTIONS, exoPlayerForwarder, mediaCodecSelector); verify(exoPlayer).addListener(exoPlayerForwarder.exoPlayerEventListener()); } @Test public void whenLoadingVideo_thenSetsAnalyticsListener() { facade.loadVideo(surfaceViewHolder, drmSessionCreator, uri, OPTIONS, exoPlayerForwarder, mediaCodecSelector); verify(exoPlayer).addAnalyticsListener(exoPlayerForwarder.analyticsListener()); } @Test public void givenSurfaceContainerContainsSurfaceView_whenLoadingVideo_thenSetsSurfaceViewOnExoPlayer() { facade.loadVideo(surfaceViewHolder, drmSessionCreator, uri, OPTIONS, exoPlayerForwarder, mediaCodecSelector); verify(exoPlayer).setVideoSurfaceView(surfaceView); } @Test public void givenSurfaceContainerContainsTextureView_whenLoadingVideo_thenSetsTextureViewOnExoPlayer() { facade.loadVideo(textureViewHolder, drmSessionCreator, uri, OPTIONS, exoPlayerForwarder, mediaCodecSelector); verify(exoPlayer).setVideoTextureView(textureView); } @Test public void givenLollipopDevice_whenLoadingVideo_thenSetsMovieAudioAttributesOnExoPlayer() { given(androidDeviceVersion.isLollipopTwentyOneOrAbove()).willReturn(true); facade.loadVideo(surfaceViewHolder, drmSessionCreator, uri, OPTIONS, exoPlayerForwarder, mediaCodecSelector); AudioAttributes expectedMovieAudioAttributes = new AudioAttributes.Builder() .setContentType(C.CONTENT_TYPE_MOVIE) .build(); verify(exoPlayer).setAudioAttributes(expectedMovieAudioAttributes); } @Test public void givenNonLollipopDevice_whenLoadingVideo_thenDoesNotSetAudioAttributesOnExoPlayer() { given(androidDeviceVersion.isLollipopTwentyOneOrAbove()).willReturn(false); facade.loadVideo(surfaceViewHolder, drmSessionCreator, uri, OPTIONS, exoPlayerForwarder, mediaCodecSelector); verify(exoPlayer, never()).setAudioAttributes(any(AudioAttributes.class)); } @Test public void givenMediaSource_whenLoadingVideo_thenPreparesInternalExoPlayer() { MediaSource mediaSource = givenMediaSource(OPTIONS); facade.loadVideo(surfaceViewHolder, drmSessionCreator, uri, OPTIONS, exoPlayerForwarder, mediaCodecSelector); verify(exoPlayer).prepare(mediaSource, RESET_POSITION, DO_NOT_RESET_STATE); } @Test public void givenInitialPosition_whenLoadingVideo_thenPerformsSeekBeforePreparing() { Options options = OPTIONS.toOptionsBuilder() .withInitialPositionInMillis(TWENTY_FIVE_SECONDS_IN_MILLIS) .build(); MediaSource mediaSource = givenMediaSource(options); facade.loadVideo(surfaceViewHolder, drmSessionCreator, uri, options, exoPlayerForwarder, mediaCodecSelector); InOrder inOrder = inOrder(exoPlayer); inOrder.verify(exoPlayer).seekTo(TWENTY_FIVE_SECONDS_IN_MILLIS); inOrder.verify(exoPlayer).prepare(mediaSource, DO_NOT_RESET_POSITION, DO_NOT_RESET_STATE); } @Test public void givenNoInitialPosition_whenLoadingVideo_thenDoesNotPerformSeekBeforePreparing() { MediaSource mediaSource = givenMediaSource(OPTIONS); facade.loadVideo(surfaceViewHolder, drmSessionCreator, uri, OPTIONS, exoPlayerForwarder, mediaCodecSelector); InOrder inOrder = inOrder(exoPlayer); inOrder.verify(exoPlayer, never()).seekTo(TWENTY_FIVE_SECONDS_IN_MILLIS); inOrder.verify(exoPlayer).prepare(mediaSource, RESET_POSITION, DO_NOT_RESET_STATE); } @Test public void whenQueryingIsPlaying_thenReturnsFalse() { boolean isPlaying = facade.isPlaying(); assertThat(isPlaying).isFalse(); } @Test public void whenQueryingPlayheadPosition_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches("Video must be loaded before trying to interact with the player", IllegalStateException.class)); facade.playheadPositionInMillis(); } @Test public void whenQueryingMediaDuration_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches("Video must be loaded before trying to interact with the player", IllegalStateException.class)); facade.mediaDurationInMillis(); } @Test public void whenQueryingBufferPercentage_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches("Video must be loaded before trying to interact with the player", IllegalStateException.class)); facade.bufferPercentage(); } @Test public void whenPausing_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches("Video must be loaded before trying to interact with the player", IllegalStateException.class)); facade.pause(); } @Test public void whenSeeking_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches("Video must be loaded before trying to interact with the player", IllegalStateException.class)); facade.seekTo(ANY_POSITION); } @Test public void whenSelectingAudioTrack_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches("Video must be loaded before trying to interact with the player", IllegalStateException.class)); PlayerAudioTrack audioTrack = mock(PlayerAudioTrack.class); facade.selectAudioTrack(audioTrack); } @Test public void whenGettingAudioTracks_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches("Video must be loaded before trying to interact with the player", IllegalStateException.class)); given(trackSelector.getAudioTracks(any(RendererTypeRequester.class))).willReturn(AUDIO_TRACKS); facade.getAudioTracks(); } @Test public void selectSubtitleTrack_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches("Video must be loaded before trying to interact with the player", IllegalStateException.class)); PlayerSubtitleTrack subtitleTrack = mock(PlayerSubtitleTrack.class); facade.selectSubtitleTrack(subtitleTrack); } @Test public void whenSetVolume_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches("Video must be loaded before trying to interact with the player", IllegalStateException.class)); facade.setVolume(ANY_VOLUME); } @Test public void whenGetVolume_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches("Video must be loaded before trying to interact with the player", IllegalStateException.class)); facade.getVolume(); } } public static class GivenVideoIsLoaded extends Base { private static final PlayerAudioTrack PLAYER_AUDIO_TRACK = PlayerAudioTrackFixture.aPlayerAudioTrack().build(); private static final AudioTracks AUDIO_TRACKS = AudioTracks.from(Collections.singletonList(PLAYER_AUDIO_TRACK)); private static final PlayerVideoTrack PLAYER_VIDEO_TRACK = PlayerVideoTrackFixture.aPlayerVideoTrack().build(); private static final List VIDEO_TRACKS = Collections.singletonList(PLAYER_VIDEO_TRACK); @Override public void setUp() { super.setUp(); givenPlayerIsLoaded(); } private void givenPlayerIsLoaded() { givenMediaSource(OPTIONS); facade.loadVideo(surfaceViewHolder, drmSessionCreator, uri, OPTIONS, exoPlayerForwarder, mediaCodecSelector); } @Test public void whenResetting_thenReleasesUnderlyingPlayer() { facade.release(); verify(exoPlayer).release(); } @Test public void whenPausing_thenSetsPlayWhenReadyToFalse() { facade.pause(); verify(exoPlayer).setPlayWhenReady(DO_NOT_PLAY_WHEN_READY); } @Test public void whenSeeking_thenSeeksToPosition() { long videoPositionInMillis = TWO_MINUTES_IN_MILLIS; facade.seekTo(videoPositionInMillis); verify(exoPlayer).seekTo(videoPositionInMillis); } @Test public void whenStartingPlay_thenSetsPlayWhenReadyToTrue() { facade.play(); verify(exoPlayer).setPlayWhenReady(PLAY_WHEN_READY); } @Test public void whenStartingPlayAtVideoPosition_thenSeeksToPosition() { facade.play(TWO_MINUTES_IN_MILLIS); verify(exoPlayer).seekTo(TWO_MINUTES_IN_MILLIS); } @Test public void whenStartingPlayAtVideoPosition_thenSetsPlayWhenReadyToTrue() { facade.play(TWO_MINUTES_IN_MILLIS); verify(exoPlayer).setPlayWhenReady(PLAY_WHEN_READY); } @Test public void givenExoPlayerIsReadyToPlay_whenQueryingIsPlaying_thenReturnsTrue() { given(exoPlayer.getPlayWhenReady()).willReturn(IS_PLAYING); boolean isPlaying = facade.isPlaying(); assertThat(isPlaying).isTrue(); } @Test public void whenGettingPlayheadPosition_thenReturnsCurrentPosition() { given(exoPlayer.getCurrentPosition()).willReturn(TWO_MINUTES_IN_MILLIS); long playheadPositionInMillis = facade.playheadPositionInMillis(); assertThat(playheadPositionInMillis).isEqualTo(TWO_MINUTES_IN_MILLIS); } @Test public void whenGettingMediaDuration_thenReturnsDuration() { given(exoPlayer.getDuration()).willReturn(TEN_MINUTES_IN_MILLIS); long videoDurationInMillis = facade.mediaDurationInMillis(); assertThat(videoDurationInMillis).isEqualTo(TEN_MINUTES_IN_MILLIS); } @Test public void whenGettingBufferPercentage_thenReturnsBufferPercentage() { given(exoPlayer.getBufferedPercentage()).willReturn(TEN_PERCENT); int bufferPercentage = facade.bufferPercentage(); assertThat(bufferPercentage).isEqualTo(TEN_PERCENT); } @Test public void whenSelectingAudioTrack_thenDelegatesToTrackSelector() { PlayerAudioTrack audioTrack = mock(PlayerAudioTrack.class); facade.selectAudioTrack(audioTrack); verify(trackSelector).selectAudioTrack(audioTrack, rendererTypeRequester); } @Test public void givenSelectingAudioTrackSuceeds_whenSelectingAudioTrack_thenReturnsTrue() { PlayerAudioTrack audioTrack = mock(PlayerAudioTrack.class); given(trackSelector.selectAudioTrack(audioTrack, rendererTypeRequester)).willReturn(true); boolean success = facade.selectAudioTrack(audioTrack); assertThat(success).isTrue(); } @Test public void givenSelectingAudioTrackFails_whenSelectingAudioTrack_thenReturnsFalse() { PlayerAudioTrack audioTrack = mock(PlayerAudioTrack.class); given(trackSelector.selectAudioTrack(audioTrack, rendererTypeRequester)).willReturn(false); boolean success = facade.selectAudioTrack(audioTrack); assertThat(success).isFalse(); } @Test public void whenSelectingSubtitlesTrack_thenDelegatesToTrackSelector() { PlayerSubtitleTrack subtitleTrack = mock(PlayerSubtitleTrack.class); facade.selectSubtitleTrack(subtitleTrack); verify(trackSelector).selectTextTrack(subtitleTrack, rendererTypeRequester); } @Test public void givenSelectingTextTrackSuceeds_whenSelectingSubtitlesTrack_thenReturnsTrue() { PlayerSubtitleTrack subtitleTrack = mock(PlayerSubtitleTrack.class); given(trackSelector.selectTextTrack(subtitleTrack, rendererTypeRequester)).willReturn(true); boolean success = facade.selectSubtitleTrack(subtitleTrack); assertThat(success).isTrue(); } @Test public void givenSelectingTextTrackFails_whenSelectingSubtitlesTrack_thenReturnsFalse() { PlayerSubtitleTrack subtitleTrack = mock(PlayerSubtitleTrack.class); given(trackSelector.selectTextTrack(subtitleTrack, rendererTypeRequester)).willReturn(false); boolean success = facade.selectSubtitleTrack(subtitleTrack); assertThat(success).isFalse(); } @Test public void whenGettingAudioTracks_thenDelegatesToTrackSelector() { given(trackSelector.getAudioTracks(any(RendererTypeRequester.class))).willReturn(AUDIO_TRACKS); AudioTracks audioTracks = facade.getAudioTracks(); assertThat(audioTracks).isEqualTo(AUDIO_TRACKS); } @Test public void whenGettingSelectedVideoTrack_thenDelegatesTrackSelector() { given(trackSelector.getSelectedVideoTrack(eq(exoPlayer), any(RendererTypeRequester.class), any(ContentType.class))).willReturn(Optional.of(PLAYER_VIDEO_TRACK)); Optional selectedVideoTrack = facade.getSelectedVideoTrack(); assertThat(selectedVideoTrack).isEqualTo(Optional.of(PLAYER_VIDEO_TRACK)); } @Test public void whenSelectingVideoTrack_thenDelegatesToTrackSelector() { given(trackSelector.selectVideoTrack(eq(PLAYER_VIDEO_TRACK), any(RendererTypeRequester.class))).willReturn(SELECTED); boolean selectedVideoTrack = facade.selectVideoTrack(PLAYER_VIDEO_TRACK); assertThat(selectedVideoTrack).isTrue(); } @Test public void whenGettingVideoTracks_thenDelegatesToTrackSelector() { given(trackSelector.getVideoTracks(any(RendererTypeRequester.class), any(ContentType.class))).willReturn(VIDEO_TRACKS); List videoTracks = facade.getVideoTracks(); assertThat(videoTracks).isEqualTo(VIDEO_TRACKS); } @Test public void whenSetRepeatingTrue_thenSetsRepeatModeAll() { facade.setRepeating(true); verify(exoPlayer).setRepeatMode(Player.REPEAT_MODE_ALL); } @Test public void whenSetRepeatingFalse_thenSetsRepeatModeOff() { facade.setRepeating(false); verify(exoPlayer).setRepeatMode(Player.REPEAT_MODE_OFF); } @Test public void whenSetVolume_thenSetsPlayerVolume() { facade.setVolume(ANY_VOLUME); verify(exoPlayer).setVolume(ANY_VOLUME); } @Test public void whenGetVolume_thenGetsPlayerVolume() { given(exoPlayer.getVolume()).willReturn(ANY_VOLUME); float currentVolume = facade.getVolume(); assertThat(currentVolume).isEqualTo(ANY_VOLUME); } } public abstract static class Base { static final float ANY_VOLUME = 0.5f; @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock BandwidthMeterCreator bandwidthMeterCreator; @Mock DefaultBandwidthMeter defaultBandwidthMeter; @Mock AndroidDeviceVersion androidDeviceVersion; @Mock SimpleExoPlayer exoPlayer; @Mock MediaSourceFactory mediaSourceFactory; @Mock ExoPlayerForwarder exoPlayerForwarder; @Mock CompositeTrackSelectorCreator trackSelectorCreator; @Mock CompositeTrackSelector trackSelector; @Mock Uri uri; @Mock RendererTypeRequester rendererTypeRequester; @Mock RendererTypeRequesterCreator rendererTypeRequesterCreator; @Mock DrmSessionCreator drmSessionCreator; @Mock DefaultDrmSessionEventListener drmSessionEventListener; @Mock MediaSourceEventListener mediaSourceEventListener; @Mock MediaCodecSelector mediaCodecSelector; @Mock SurfaceView surfaceView; @Mock TextureView textureView; PlayerSurfaceHolder surfaceViewHolder; PlayerSurfaceHolder textureViewHolder; ExoPlayerFacade facade; @Before public void setUp() { ExoPlayerCreator exoPlayerCreator = mock(ExoPlayerCreator.class); given(exoPlayerForwarder.drmSessionEventListener()).willReturn(drmSessionEventListener); given(exoPlayerForwarder.mediaSourceEventListener()).willReturn(mediaSourceEventListener); given(bandwidthMeterCreator.create(anyLong())).willReturn(defaultBandwidthMeter); given(trackSelectorCreator.create(any(Options.class), eq(defaultBandwidthMeter))).willReturn(trackSelector); given(exoPlayerCreator.create(drmSessionCreator, drmSessionEventListener, mediaCodecSelector, trackSelector.trackSelector())).willReturn(exoPlayer); willDoNothing().given(exoPlayer).seekTo(anyInt()); given(rendererTypeRequesterCreator.createfrom(exoPlayer)).willReturn(rendererTypeRequester); facade = new ExoPlayerFacade( bandwidthMeterCreator, androidDeviceVersion, mediaSourceFactory, trackSelectorCreator, exoPlayerCreator, rendererTypeRequesterCreator ); given(surfaceView.getHolder()).willReturn(mock(SurfaceHolder.class)); surfaceViewHolder = PlayerSurfaceHolder.create(surfaceView); textureViewHolder = PlayerSurfaceHolder.create(textureView); } MediaSource givenMediaSource(Options options) { MediaSource mediaSource = mock(MediaSource.class); given( mediaSourceFactory.create( options, uri, mediaSourceEventListener, defaultBandwidthMeter ) ).willReturn(mediaSource); return mediaSource; } } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerInformationTest.java ================================================ package com.novoda.noplayer.internal.exoplayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.novoda.noplayer.PlayerType; import org.junit.Before; import org.junit.Test; import static org.fest.assertions.api.Assertions.assertThat; public class ExoPlayerInformationTest { private ExoPlayerInformation playerInformation; @Before public void setUp() { playerInformation = new ExoPlayerInformation(); } @Test public void whenReadingName_thenReturnsExoPlayer() { String name = playerInformation.getName(); assertThat(name).isEqualTo("ExoPlayer"); } @Test public void whenReadingVersion_thenReturnsExoPlayerLibraryVersion() { String version = playerInformation.getVersion(); assertThat(version).isEqualTo(ExoPlayerLibraryInfo.VERSION); } @Test public void whenPlayerType_thenReturnsExoPlayer() { PlayerType playerType = playerInformation.getPlayerType(); assertThat(playerType).isEqualTo(PlayerType.EXO_PLAYER); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerTwoImplTest.java ================================================ package com.novoda.noplayer.internal.exoplayer; import android.net.Uri; import android.view.View; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.text.Cue; import com.novoda.noplayer.ContentType; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.NoPlayer.StateChangedListener; import com.novoda.noplayer.Options; import com.novoda.noplayer.OptionsBuilder; import com.novoda.noplayer.PlayerInformation; import com.novoda.noplayer.PlayerSurfaceHolder; import com.novoda.noplayer.PlayerType; import com.novoda.noplayer.PlayerView; import com.novoda.noplayer.internal.Heart; import com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreator; import com.novoda.noplayer.internal.exoplayer.forwarder.ExoPlayerForwarder; import com.novoda.noplayer.internal.listeners.PlayerListenersHolder; import com.novoda.noplayer.model.LoadTimeout; import com.novoda.noplayer.model.PlayerSubtitleTrack; import com.novoda.noplayer.model.TextCues; import com.novoda.noplayer.model.Timeout; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.experimental.runners.Enclosed; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.mockito.stubbing.Answer; import java.util.Arrays; import java.util.List; import static android.provider.CalendarContract.CalendarCache.URI; import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @RunWith(Enclosed.class) public class ExoPlayerTwoImplTest { private static final long TWO_MINUTES_IN_MILLIS = 120000; private static final long TEN_SECONDS = 10; private static final int WIDTH = 120; private static final int HEIGHT = 160; private static final int ANY_ROTATION_DEGREES = 30; private static final int ANY_PIXEL_WIDTH_HEIGHT = 75; private static final boolean IS_BEATING = true; private static final boolean IS_NOT_BEATING = false; private static final Options OPTIONS = new OptionsBuilder().withContentType(ContentType.DASH).build(); private static final Timeout ANY_TIMEOUT = Timeout.fromSeconds(TEN_SECONDS); private static final NoPlayer.LoadTimeoutCallback ANY_LOAD_TIMEOUT_CALLBACK = new NoPlayer.LoadTimeoutCallback() { @Override public void onLoadTimeout() { } }; private static final int INDEX_INTERNAL_VIDEO_SIZE_CHANGED_LISTENER = 0; public static class GivenVideoNotLoaded extends Base { @Rule public ExpectedException thrown = ExpectedException.none(); @Test public void whenInitialisingPlayer_thenBindsListenersToForwarder() { player.initialise(); verify(forwarder).bind(preparedListener, player); verify(forwarder).bind(completionListener, stateChangedListener); verify(forwarder).bind(errorListener); verify(forwarder).bind(bufferStateListener); verify(forwarder).bind(videoSizeChangedListener); verify(forwarder).bind(bitrateChangedListener); verify(forwarder).bind(infoListener); } @Test public void whenInitialisingPlayer_thenBindsHeart() { player.initialise(); verify(listenersHolder).getHeartbeatCallbacks(); verify(heart).bind(any(Heart.Heartbeat.class)); } @Test public void givenPlayerIsInitialised_whenVideoIsPrepared_thenCancelsTimeout() { player.initialise(); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(NoPlayer.PreparedListener.class); verify(listenersHolder).addPreparedListener(argumentCaptor.capture()); NoPlayer.PreparedListener preparedListener = argumentCaptor.getValue(); preparedListener.onPrepared(player); verify(loadTimeout).cancel(); } @Test public void givenPlayerIsInitialised_whenVideoHasError_thenPlayerResourcesAreReleased_andNotListeners() { player.initialise(); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(NoPlayer.ErrorListener.class); verify(listenersHolder).addErrorListener(argumentCaptor.capture()); NoPlayer.ErrorListener errorListener = argumentCaptor.getValue(); errorListener.onError(mock(NoPlayer.PlayerError.class)); verify(listenersHolder).resetState(); verify(loadTimeout).cancel(); verify(heart).stopBeatingHeart(); verify(exoPlayerFacade).release(); verify(listenersHolder, never()).clear(); verify(stateChangedListener, never()).onVideoStopped(); } @Test public void givenPlayerIsInitialised_andPlayerViewIsAttached_whenVideoSizeChanges_thenPlayerVideoWidthAndHeightMatches() { player.initialise(); player.attach(playerView); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(NoPlayer.VideoSizeChangedListener.class); verify(listenersHolder, times(2)).addVideoSizeChangedListener(argumentCaptor.capture()); NoPlayer.VideoSizeChangedListener videoSizeChangedListener = argumentCaptor.getAllValues().get(INDEX_INTERNAL_VIDEO_SIZE_CHANGED_LISTENER); videoSizeChangedListener.onVideoSizeChanged(WIDTH, HEIGHT, ANY_ROTATION_DEGREES, ANY_PIXEL_WIDTH_HEIGHT); int actualWidth = player.videoWidth(); int actualHeight = player.videoHeight(); assertThat(actualWidth).isEqualTo(WIDTH); assertThat(actualHeight).isEqualTo(HEIGHT); } @Test public void givenPlayerIsInitialised_whenAttachingPlayerView_thenAddsPlayerViewVideoSizeChangedListenerToListenersHolder() { player.initialise(); player.attach(playerView); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(NoPlayer.VideoSizeChangedListener.class); verify(listenersHolder, times(2)).addVideoSizeChangedListener(argumentCaptor.capture()); NoPlayer.VideoSizeChangedListener videoSizeChangedListener = argumentCaptor.getAllValues().get(1); assertThat(videoSizeChangedListener).isSameAs(playerView.getVideoSizeChangedListener()); } @Test public void whenStopping_thenPlayerResourcesAreReleased() { player.stop(); verify(listenersHolder).resetState(); verify(stateChangedListener).onVideoStopped(); verify(loadTimeout).cancel(); verify(heart).stopBeatingHeart(); verify(exoPlayerFacade).release(); } @Test public void whenReleasing_thenPlayerResourcesAreReleased() { player.release(); verify(listenersHolder).resetState(); verify(stateChangedListener).onVideoStopped(); verify(loadTimeout).cancel(); verify(heart).stopBeatingHeart(); verify(exoPlayerFacade).release(); verify(listenersHolder).clear(); } @Test public void givenAttachedPlayerView_whenStopping_thenPlayerResourcesAreReleased() { player.attach(playerView); player.stop(); verify(listenersHolder).resetState(); verify(stateChangedListener).onVideoStopped(); verify(loadTimeout).cancel(); verify(heart).stopBeatingHeart(); verify(containerView).setVisibility(View.GONE); verify(exoPlayerFacade).release(); } @Test public void givenAttachedPlayerView_whenReleasing_thenPlayerResourcesAreReleased() { player.attach(playerView); player.release(); verify(listenersHolder).resetState(); verify(stateChangedListener).onVideoStopped(); verify(loadTimeout).cancel(); verify(heart).stopBeatingHeart(); verify(containerView).setVisibility(View.GONE); verify(exoPlayerFacade).release(); verify(listenersHolder).clear(); } @Test public void whenLoadingVideo_thenDelegatesLoadingToFacade() { player.attach(playerView); player.loadVideo(uri, OPTIONS); verify(exoPlayerFacade).loadVideo(playerView.getPlayerSurfaceHolder(), drmSessionCreator, uri, OPTIONS, forwarder, mediaCodecSelector); } @Test public void whenLoadingVideoWithTimeout_thenDelegatesLoadingToFacade() { player.attach(playerView); player.loadVideoWithTimeout(uri, OPTIONS, ANY_TIMEOUT, ANY_LOAD_TIMEOUT_CALLBACK); verify(exoPlayerFacade).loadVideo(playerView.getPlayerSurfaceHolder(), drmSessionCreator, uri, OPTIONS, forwarder, mediaCodecSelector); } @Test public void whenLoadingVideoWithTimeout_thenStartsLoadTimeout() { player.attach(playerView); player.loadVideoWithTimeout(uri, OPTIONS, ANY_TIMEOUT, ANY_LOAD_TIMEOUT_CALLBACK); verify(loadTimeout).start(ANY_TIMEOUT, ANY_LOAD_TIMEOUT_CALLBACK); } @Test public void whenGettingPlayerInformation_thenReturnsPlayerInformation() { PlayerInformation playerInformation = player.getPlayerInformation(); assertThat(playerInformation.getPlayerType()).isEqualTo(PlayerType.EXO_PLAYER); assertThat(playerInformation.getVersion()).isEqualTo(ExoPlayerLibraryInfo.VERSION); } @Test public void whenQueryingIsPlaying_thenReturnsFalse() { boolean isPlaying = player.isPlaying(); assertThat(isPlaying).isFalse(); } @Test public void whenAttachingPlayerView_thenAddsVideoSizeChangedListener() { player.attach(playerView); verify(listenersHolder).addVideoSizeChangedListener(videoSizeChangedListener); } @Test public void whenAttachingPlayerView_thenAddsStateChangedListener() { player.attach(playerView); verify(listenersHolder).addStateChangedListener(stateChangeListener); } @Test public void givenAttachedPlayerView_whenDetachingPlayerView_thenRemovesVideoSizeChangedListener() { player.attach(playerView); player.detach(playerView); verify(listenersHolder).removeVideoSizeChangedListener(videoSizeChangedListener); } @Test public void givenAttachedPlayerView_whenDetachingPlayerView_thenRemovesStateChangedListener() { player.attach(playerView); player.detach(playerView); verify(listenersHolder).removeStateChangedListener(stateChangeListener); } @Test public void givenAttachedPlayerView_whenLoadingVideo_thenMakesContainerVisible() { player.attach(playerView); player.loadVideo(uri, OPTIONS); verify(containerView).setVisibility(View.VISIBLE); } @Test public void givenPlayerHasPlayedVideo_whenLoadingVideo_thenPlayerIsReleased_andNotListeners() { given(exoPlayerFacade.hasPlayedContent()).willReturn(true); player.attach(playerView); player.loadVideo(URI, OPTIONS); verify(stateChangedListener).onVideoStopped(); verify(loadTimeout).cancel(); verify(heart).stopBeatingHeart(); verify(exoPlayerFacade).release(); verify(listenersHolder, never()).clear(); } @Test public void givenPlayerHasPlayedVideo_whenLoadingVideoWithTimeout_thenPlayerResourcesAreReleased_andNotListeners() { given(exoPlayerFacade.hasPlayedContent()).willReturn(true); player.attach(playerView); player.loadVideoWithTimeout(URI, OPTIONS, ANY_TIMEOUT, ANY_LOAD_TIMEOUT_CALLBACK); verify(stateChangedListener).onVideoStopped(); verify(loadTimeout).cancel(); verify(heart).stopBeatingHeart(); verify(exoPlayerFacade).release(); verify(listenersHolder, never()).clear(); } @Test public void givenPlayerHasNotPlayedVideo_whenLoadingVideo_thenPlayerResourcesAreNotReleased() { given(exoPlayerFacade.hasPlayedContent()).willReturn(false); player.attach(playerView); player.loadVideo(URI, OPTIONS); verify(stateChangedListener, never()).onVideoStopped(); verify(loadTimeout, never()).cancel(); verify(heart, never()).stopBeatingHeart(); verify(exoPlayerFacade, never()).release(); } @Test public void givenPlayerHasNotPlayedVideo_whenLoadingVideoWithTimeout_thenPlayerResourcesAreNotReleased() { given(exoPlayerFacade.hasPlayedContent()).willReturn(false); player.attach(playerView); player.loadVideoWithTimeout(URI, OPTIONS, ANY_TIMEOUT, ANY_LOAD_TIMEOUT_CALLBACK); verify(stateChangedListener, never()).onVideoStopped(); verify(loadTimeout, never()).cancel(); verify(heart, never()).stopBeatingHeart(); verify(exoPlayerFacade, never()).release(); } } public static class GivenAttachedAndVideoIsLoaded extends Base { private static final float ANY_VOLUME = 0.4f; @Override public void setUp() { super.setUp(); player.attach(playerView); player.loadVideo(uri, OPTIONS); } @Test public void whenLoadingVideo_thenAddsStateChangedListenerToListenersHolder() { player.loadVideo(uri, OPTIONS); verify(listenersHolder).addStateChangedListener(playerView.getStateChangedListener()); } @Test public void whenLoadingVideo_thenAddsVideoSizeChangedListenerToListenersHolder() { player.loadVideo(uri, OPTIONS); verify(listenersHolder).addVideoSizeChangedListener(playerView.getVideoSizeChangedListener()); } @Test public void whenReleasing_thenResetsFacade() { player.release(); verify(exoPlayerFacade).release(); } @Test public void whenStartingPlayback_thenStartsBeatingHeart() { player.play(); verify(heart).startBeatingHeart(); } @Test public void whenPausing_thenNotifiesStateListenersThatVideoIsPaused() { player.pause(); verify(stateChangedListener).onVideoPaused(); } @Test public void givenHeartIsBeating_whenPausing_thenStopsBeatingHeart() { given(heart.isBeating()).willReturn(IS_BEATING); player.pause(); verify(heart).stopBeatingHeart(); } @Test public void givenHeartIsBeating_whenPausing_thenForcesHeartBeat() { given(heart.isBeating()).willReturn(IS_BEATING); player.pause(); verify(heart).forceBeat(); } @Test public void givenHeartIsNotBeating_whenPausing_thenDoesNotStopBeatingHeart() { given(heart.isBeating()).willReturn(IS_NOT_BEATING); player.pause(); verify(heart, never()).stopBeatingHeart(); } @Test public void givenHeartIsNotBeating_whenPausing_thenDoesNotForceHeartBeat() { given(heart.isBeating()).willReturn(IS_NOT_BEATING); player.pause(); verify(heart, never()).forceBeat(); } @Test public void whenSeeking_thenSeeksToPosition() { long seekPositionInMillis = TWO_MINUTES_IN_MILLIS; player.seekTo(seekPositionInMillis); verify(exoPlayerFacade).seekTo(seekPositionInMillis); } @Test public void whenStartingPlayback_andSurfaceHolderIsReady_thenPlaysFacadeWithSurfaceHolder() { player.play(); verify(exoPlayerFacade).play(); } @Test public void whenStartingPlayAtVideoPosition_thenSeeksToPosition() { player.playAt(TWO_MINUTES_IN_MILLIS); verify(exoPlayerFacade).seekTo(TWO_MINUTES_IN_MILLIS); } @Test public void whenStartingPlayAtVideoPosition_thenStartsBeatingHeart() { player.playAt(TWO_MINUTES_IN_MILLIS); verify(heart).startBeatingHeart(); } @Test public void whenStartingPlay_thenNotifiesStateListenersThatVideoIsPlaying() { player.play(); verify(stateChangedListener).onVideoPlaying(); } @Test public void whenStartingPlayAtVideoPosition_thenNotifiesStateListenersThatVideoIsPlaying() { player.playAt(TWO_MINUTES_IN_MILLIS); verify(stateChangedListener).onVideoPlaying(); } @Test public void whenSelectingSubtitlesTrack_thenShowsPlayerSubtitlesView() { PlayerSubtitleTrack playerSubtitleTrack = PlayerSubtitleTrackFixture.anInstance().build(); player.showSubtitleTrack(playerSubtitleTrack); verify(playerView).showSubtitles(); } @Test public void givenSelectingSubtitleTrackSuceeds_whenSelectingSubtitlesTrack_thenReturnsTrue() { PlayerSubtitleTrack playerSubtitleTrack = mock(PlayerSubtitleTrack.class); given(exoPlayerFacade.selectSubtitleTrack(playerSubtitleTrack)).willReturn(true); boolean success = player.showSubtitleTrack(playerSubtitleTrack); assertThat(success).isTrue(); } @Test public void givenSelectingSubtitleTrackFails_whenSelectingSubtitlesTrack_thenReturnsFalse() { PlayerSubtitleTrack playerSubtitleTrack = mock(PlayerSubtitleTrack.class); given(exoPlayerFacade.selectSubtitleTrack(playerSubtitleTrack)).willReturn(false); boolean success = player.showSubtitleTrack(playerSubtitleTrack); assertThat(success).isFalse(); } @Test public void givenPlayerHasLoadedSubtitleCues_whenSelectingSubtitlesTrack_thenSetsSubtitleCuesOnView() { TextCues textCues = givenPlayerHasLoadedSubtitleCues(); PlayerSubtitleTrack playerSubtitleTrack = PlayerSubtitleTrackFixture.anInstance().build(); player.showSubtitleTrack(playerSubtitleTrack); verify(playerView).setSubtitleCue(textCues); } private TextCues givenPlayerHasLoadedSubtitleCues() { final List cueList = Arrays.asList(new Cue("first cue"), new Cue("secondCue")); doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) { TextRendererOutput output = invocation.getArgument(0); output.output().onCues(cueList); return null; } }).when(exoPlayerFacade).setSubtitleRendererOutput(any(TextRendererOutput.class)); return ExoPlayerCueMapper.map(cueList); } @Test public void whenClearingSubtitles_thenHidesPlayerSubtitlesView() { player.hideSubtitleTrack(); verify(playerView).hideSubtitles(); } @Test public void whenSetRepeating_thenSetRepeating() { player.setRepeating(false); verify(exoPlayerFacade).setRepeating(false); } @Test public void whenSetVolume_thenSetVolumeOnExoPlayer() { player.setVolume(ANY_VOLUME); verify(exoPlayerFacade).setVolume(ANY_VOLUME); } @Test public void whenGetVolume_thenReturnVolumeFromExoPlayer() { given(exoPlayerFacade.getVolume()).willReturn(ANY_VOLUME); float currentVolume = player.getVolume(); assertThat(currentVolume).isEqualTo(ANY_VOLUME); } } public abstract static class Base { @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock ExoPlayerForwarder forwarder; @Mock LoadTimeout loadTimeout; @Mock Heart heart; @Mock Uri uri; @Mock PlayerView playerView; @Mock StateChangedListener stateChangeListener; @Mock NoPlayer.VideoSizeChangedListener videoSizeChangedListener; @Mock PlayerListenersHolder listenersHolder; @Mock NoPlayer.ErrorListener errorListener; @Mock NoPlayer.PreparedListener preparedListener; @Mock NoPlayer.BufferStateListener bufferStateListener; @Mock NoPlayer.CompletionListener completionListener; @Mock NoPlayer.StateChangedListener stateChangedListener; @Mock NoPlayer.InfoListener infoListener; @Mock NoPlayer.BitrateChangedListener bitrateChangedListener; @Mock ExoPlayerFacade exoPlayerFacade; @Mock DrmSessionCreator drmSessionCreator; @Mock MediaCodecSelector mediaCodecSelector; @Mock View containerView; @Mock PlayerSurfaceHolder playerSurfaceHolder; ExoPlayerTwoImpl player; @Before public void setUp() { given(playerView.getPlayerSurfaceHolder()).willReturn(playerSurfaceHolder); given(playerView.getStateChangedListener()).willReturn(stateChangeListener); given(playerView.getVideoSizeChangedListener()).willReturn(videoSizeChangedListener); given(playerView.getContainerView()).willReturn(containerView); given(listenersHolder.getErrorListeners()).willReturn(errorListener); given(listenersHolder.getPreparedListeners()).willReturn(preparedListener); given(listenersHolder.getBufferStateListeners()).willReturn(bufferStateListener); given(listenersHolder.getCompletionListeners()).willReturn(completionListener); given(listenersHolder.getStateChangedListeners()).willReturn(stateChangedListener); given(listenersHolder.getInfoListeners()).willReturn(infoListener); given(listenersHolder.getVideoSizeChangedListeners()).willReturn(videoSizeChangedListener); given(listenersHolder.getBitrateChangedListeners()).willReturn(bitrateChangedListener); player = new ExoPlayerTwoImpl( exoPlayerFacade, listenersHolder, forwarder, loadTimeout, heart, drmSessionCreator, mediaCodecSelector ); } } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/exoplayer/NoPlayerExoPlayerCreatorTest.java ================================================ package com.novoda.noplayer.internal.exoplayer; import android.content.Context; import com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreator; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; public class NoPlayerExoPlayerCreatorTest { private static final boolean USE_SECURE_CODEC = true; private static final boolean ALLOW_CROSS_PROTOCOL_REDIRECTS = true; @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock private ExoPlayerTwoImpl player; @Mock private Context context; @Mock private DrmSessionCreator drmSessionCreator; @Mock private NoPlayerExoPlayerCreator.InternalCreator internalCreator; private NoPlayerExoPlayerCreator creator; @Before public void setUp() { given(internalCreator.create(context, drmSessionCreator, USE_SECURE_CODEC, ALLOW_CROSS_PROTOCOL_REDIRECTS)).willReturn(player); creator = new NoPlayerExoPlayerCreator(internalCreator); } @Test public void whenCreatingExoPlayerTwo_thenInitialisesPlayer() { creator.createExoPlayer(context, drmSessionCreator, USE_SECURE_CODEC, ALLOW_CROSS_PROTOCOL_REDIRECTS); verify(player).initialise(); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/exoplayer/PlayerSubtitleTrackFixture.java ================================================ package com.novoda.noplayer.internal.exoplayer; import com.novoda.noplayer.model.PlayerSubtitleTrack; class PlayerSubtitleTrackFixture { private int groupIndex = 0; private int formatIndex = 0; private String trackId = "trackId"; private String language = "language"; private String mimeType = "text/vtt"; private int numberOfChannels = 1; private int frequency = 4; private PlayerSubtitleTrackFixture() { // use anInstance() to get an instance } static PlayerSubtitleTrackFixture anInstance() { return new PlayerSubtitleTrackFixture(); } PlayerSubtitleTrackFixture withGroupIndex(int groupIndex) { this.groupIndex = groupIndex; return this; } PlayerSubtitleTrackFixture withFormatIndex(int formatIndex) { this.formatIndex = formatIndex; return this; } PlayerSubtitleTrackFixture withTrackId(String trackId) { this.trackId = trackId; return this; } PlayerSubtitleTrackFixture withLanguage(String language) { this.language = language; return this; } PlayerSubtitleTrackFixture withMimeType(String mimeType) { this.mimeType = mimeType; return this; } PlayerSubtitleTrackFixture withNumberOfChannels(int numberOfChannels) { this.numberOfChannels = numberOfChannels; return this; } PlayerSubtitleTrackFixture withFrequency(int frequency) { this.frequency = frequency; return this; } PlayerSubtitleTrack build() { return new PlayerSubtitleTrack(groupIndex, formatIndex, trackId, language, mimeType, numberOfChannels, frequency); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/exoplayer/SecurityDowngradingCodecSelectorTest.java ================================================ package com.novoda.noplayer.internal.exoplayer; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; public class SecurityDowngradingCodecSelectorTest { private static final String ANY_MIME_TYPE = "mimeType"; private static final boolean CONTENT_SECURE = true; private static final boolean CONTENT_INSECURE = false; @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock private SecurityDowngradingCodecSelector.InternalMediaCodecUtil internalMediaCodecUtil; @Test public void whenContentIsSecure_thenRequiresSecureDecoderIsFalse() throws MediaCodecUtil.DecoderQueryException { SecurityDowngradingCodecSelector securityDowngradingCodecSelector = new SecurityDowngradingCodecSelector(internalMediaCodecUtil); securityDowngradingCodecSelector.getDecoderInfos(ANY_MIME_TYPE, CONTENT_SECURE); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Boolean.class); verify(internalMediaCodecUtil).getDecoderInfos(eq(ANY_MIME_TYPE), argumentCaptor.capture()); assertThat(argumentCaptor.getValue()).isFalse(); } @Test public void whenContentIsInsecure_thenRequiresSecureDecoderIsFalse() throws MediaCodecUtil.DecoderQueryException { SecurityDowngradingCodecSelector securityDowngradingCodecSelector = new SecurityDowngradingCodecSelector(internalMediaCodecUtil); securityDowngradingCodecSelector.getDecoderInfos(ANY_MIME_TYPE, CONTENT_INSECURE); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Boolean.class); verify(internalMediaCodecUtil).getDecoderInfos(eq(ANY_MIME_TYPE), argumentCaptor.capture()); assertThat(argumentCaptor.getValue()).isFalse(); } @Test public void whenGettingPassthroughDecoderInfo_thenDelegates() throws MediaCodecUtil.DecoderQueryException { SecurityDowngradingCodecSelector securityDowngradingCodecSelector = new SecurityDowngradingCodecSelector(internalMediaCodecUtil); securityDowngradingCodecSelector.getPassthroughDecoderInfo(); verify(internalMediaCodecUtil).getPassthroughDecoderInfo(); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/exoplayer/drm/DrmSessionCreatorFactoryTest.java ================================================ package com.novoda.noplayer.internal.exoplayer.drm; import android.os.Handler; import com.novoda.noplayer.UnableToCreatePlayerException; import com.novoda.noplayer.drm.DownloadedModularDrm; import com.novoda.noplayer.drm.DrmHandler; import com.novoda.noplayer.drm.DrmType; import com.novoda.noplayer.drm.StreamingModularDrm; import com.novoda.noplayer.internal.drm.provision.ProvisionExecutorCreator; import com.novoda.noplayer.internal.utils.AndroidDeviceVersion; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import utils.ExceptionMatcher; import static org.fest.assertions.api.Assertions.assertThat; public class DrmSessionCreatorFactoryTest { private static final AndroidDeviceVersion UNSUPPORTED_MEDIA_DRM_DEVICE_VERSION = new AndroidDeviceVersion(17); private static final DrmHandler IGNORED_DRM_HANDLER = DrmHandler.NO_DRM; private static final AndroidDeviceVersion SUPPORTED_MEDIA_DRM_DEVICE = new AndroidDeviceVersion(18); @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Rule public ExpectedException thrown = ExpectedException.none(); @Mock private Handler handler; @Mock private DownloadedModularDrm downloadedModularDrm; @Mock private StreamingModularDrm streamingModularDrm; @Mock private ProvisionExecutorCreator provisionExecutorCreator; private DrmSessionCreatorFactory drmSessionCreatorFactory; @Before public void setUp() { drmSessionCreatorFactory = new DrmSessionCreatorFactory(SUPPORTED_MEDIA_DRM_DEVICE, provisionExecutorCreator, handler); } @Test public void givenDrmTypeNone_whenCreatingDrmSessionCreator_thenReturnsNoDrmSession() throws DrmSessionCreatorException { DrmSessionCreator drmSessionCreator = drmSessionCreatorFactory.createFor(DrmType.NONE, IGNORED_DRM_HANDLER); assertThat(drmSessionCreator).isInstanceOf(NoDrmSessionCreator.class); } @Test public void givenDrmTypeWidevineClassic_whenCreatingDrmSessionCreator_thenReturnsNoDrmSession() throws DrmSessionCreatorException { DrmSessionCreator drmSessionCreator = drmSessionCreatorFactory.createFor(DrmType.WIDEVINE_CLASSIC, IGNORED_DRM_HANDLER); assertThat(drmSessionCreator).isInstanceOf(NoDrmSessionCreator.class); } @Test public void givenDrmTypeWidevineModularStream_whenCreatingDrmSessionCreator_thenReturnsStreaming() throws DrmSessionCreatorException { DrmSessionCreator drmSessionCreator = drmSessionCreatorFactory.createFor(DrmType.WIDEVINE_MODULAR_STREAM, streamingModularDrm); assertThat(drmSessionCreator).isInstanceOf(StreamingDrmSessionCreator.class); } @Test public void givenDrmTypeWidevineModularStream_andAndroidVersionDoesNotSupportMediaDrmApis_whenCreatingDrmSessionCreator_thenThrowsUnableToCreatePlayerException() throws DrmSessionCreatorException { drmSessionCreatorFactory = new DrmSessionCreatorFactory(UNSUPPORTED_MEDIA_DRM_DEVICE_VERSION, provisionExecutorCreator, handler); String message = "Device must be target: 18 but was: 17 for DRM type: WIDEVINE_MODULAR_STREAM"; thrown.expect(ExceptionMatcher.matches(message, UnableToCreatePlayerException.class)); drmSessionCreatorFactory.createFor(DrmType.WIDEVINE_MODULAR_STREAM, IGNORED_DRM_HANDLER); } @Test public void givenDrmTypeWidevineModularDownload_whenCreatingDrmSessionCreator_thenReturnsDownload() throws DrmSessionCreatorException { DrmSessionCreator drmSessionCreator = drmSessionCreatorFactory.createFor(DrmType.WIDEVINE_MODULAR_DOWNLOAD, downloadedModularDrm); assertThat(drmSessionCreator).isInstanceOf(DownloadDrmSessionCreator.class); } @Test public void givenDrmTypeWidevineDownloadStream_andAndroidVersionDoesNotSupportMediaDrmApis_whenCreatingDrmSessionCreator_thenThrowsUnableToCreatePlayerException() throws DrmSessionCreatorException { drmSessionCreatorFactory = new DrmSessionCreatorFactory(UNSUPPORTED_MEDIA_DRM_DEVICE_VERSION, provisionExecutorCreator, handler); String message = "Device must be target: 18 but was: 17 for DRM type: WIDEVINE_MODULAR_DOWNLOAD"; thrown.expect(ExceptionMatcher.matches(message, UnableToCreatePlayerException.class)); drmSessionCreatorFactory.createFor(DrmType.WIDEVINE_MODULAR_DOWNLOAD, IGNORED_DRM_HANDLER); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/exoplayer/drm/LocalDrmSessionManagerTest.java ================================================ package com.novoda.noplayer.internal.exoplayer.drm; import android.media.MediaCryptoException; import android.media.MediaDrmException; import android.media.ResourceBusyException; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.support.annotation.RequiresApi; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.ExoMediaDrm; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaCryptoFixture; import com.novoda.noplayer.model.KeySetId; import java.util.Collections; import java.util.UUID; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; public class LocalDrmSessionManagerTest { private static final Looper IGNORED_LOOPER = null; private static final DrmInitData IGNORED_DRM_DATA = null; private static final KeySetId KEY_SET_ID_TO_RESTORE = KeySetId.of(new byte[12]); private static final SessionId SESSION_ID = SessionId.of(new byte[10]); private static final UUID DRM_SCHEME = UUID.randomUUID(); @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Rule public ExpectedException thrown = ExpectedException.none(); @Mock private ExoMediaDrm mediaDrm; @Mock private Handler handler; @Mock private DefaultDrmSessionManager.EventListener eventListener; private LocalDrmSessionManager localDrmSessionManager; private FrameworkMediaCrypto frameworkMediaCrypto; @Before public void setUp() throws MediaDrmException, MediaCryptoException { frameworkMediaCrypto = FrameworkMediaCryptoFixture.aFrameworkMediaCrypto().build(); given(mediaDrm.openSession()).willReturn(SESSION_ID.asBytes()); localDrmSessionManager = new LocalDrmSessionManager( KEY_SET_ID_TO_RESTORE, mediaDrm, DRM_SCHEME, handler, eventListener ); } @Test public void givenDrmDataContainsDrmScheme_whenCheckingCanAcquireSession_thenReturnsTrue() { DrmInitData.SchemeData recognisedSchemeData = new DrmInitData.SchemeData( DRM_SCHEME, "ANY_MIME_TYPE", new byte[]{} ); DrmInitData drmInitData = new DrmInitData(Collections.singletonList(recognisedSchemeData)); boolean canAcquireSession = localDrmSessionManager.canAcquireSession(drmInitData); assertThat(canAcquireSession).isTrue(); } @Test public void givenDrmDataDoesNotContainDrmScheme_whenCheckingCanAcquireSession_thenReturnsFalse() { DrmInitData.SchemeData unrecognisedSchemeData = new DrmInitData.SchemeData( UUID.randomUUID(), "ANY_MIME_TYPE", new byte[]{} ); DrmInitData drmInitData = new DrmInitData(Collections.singletonList(unrecognisedSchemeData)); boolean canAcquireSession = localDrmSessionManager.canAcquireSession(drmInitData); assertThat(canAcquireSession).isFalse(); } @Test public void givenValidMediaDrm_whenAcquiringSession_thenRestoresKeys() throws MediaCryptoException { given(mediaDrm.createMediaCrypto(SESSION_ID.asBytes())).willReturn(frameworkMediaCrypto); localDrmSessionManager.acquireSession(IGNORED_LOOPER, IGNORED_DRM_DATA); verify(mediaDrm).restoreKeys(SESSION_ID.asBytes(), KEY_SET_ID_TO_RESTORE.asBytes()); } @Test public void givenValidMediaDrm_whenAcquiringSession_thenReturnsLocalDrmSession() throws MediaCryptoException { given(mediaDrm.createMediaCrypto(SESSION_ID.asBytes())).willReturn(frameworkMediaCrypto); DrmSession drmSession = localDrmSessionManager.acquireSession(IGNORED_LOOPER, IGNORED_DRM_DATA); LocalDrmSession localDrmSession = new LocalDrmSession(frameworkMediaCrypto, KEY_SET_ID_TO_RESTORE, SESSION_ID); assertThat(drmSession).isEqualTo(localDrmSession); } @RequiresApi(api = Build.VERSION_CODES.KITKAT) @Test public void givenOpeningSessionError_whenAcquiringSession_thenNotifiesErrorEventListenerOnHandler() throws MediaDrmException { given(mediaDrm.openSession()).willThrow(new ResourceBusyException("resource is busy")); localDrmSessionManager.acquireSession(IGNORED_LOOPER, IGNORED_DRM_DATA); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Runnable.class); verify(handler).post(argumentCaptor.capture()); argumentCaptor.getValue().run(); verify(eventListener).onDrmSessionManagerError(any(DrmSession.DrmSessionException.class)); } @RequiresApi(api = Build.VERSION_CODES.KITKAT) @Test public void givenOpeningSessionError_whenAcquiringSession_thenReturnsInvalidDrmSession() throws MediaDrmException { ResourceBusyException resourceBusyException = new ResourceBusyException("resource is busy"); given(mediaDrm.openSession()).willThrow(resourceBusyException); DrmSession drmSession = localDrmSessionManager.acquireSession(IGNORED_LOOPER, IGNORED_DRM_DATA); assertThat(drmSession).isInstanceOf(InvalidDrmSession.class); assertThat(drmSession.getError().getCause()).isEqualTo(resourceBusyException); } @Test public void givenAcquiredSession_whenReleasingSession_thenClosesCurrentSession() { DrmSession drmSession = new LocalDrmSession(frameworkMediaCrypto, KEY_SET_ID_TO_RESTORE, SESSION_ID); localDrmSessionManager.releaseSession(drmSession); verify(mediaDrm).closeSession(SESSION_ID.asBytes()); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/exoplayer/error/ErrorFormatterTest.java ================================================ package com.novoda.noplayer.internal.exoplayer.error; import android.media.MediaCodec; import org.junit.Test; import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; public class ErrorFormatterTest { private static final String MESSAGE = "message"; @Test public void givenThrowable_whenFormattingMessage_thenReturnsExpectedMessageFormat() { String expectedFormat = "com.novoda.noplayer.internal.exoplayer.error.ErrorFormatterTest$IncorrectFormatThrowable: message"; String actualFormat = ErrorFormatter.formatMessage(new IncorrectFormatThrowable(MESSAGE)); assertThat(actualFormat).isEqualTo(expectedFormat); } @Test public void givenMediaCodecException_whenFormattingMessage_thenReturnsExpectedMessageFormat() { MediaCodec.CodecException codecException = mock(MediaCodec.CodecException.class); given(codecException.getDiagnosticInfo()).willReturn("android.media.MediaCodec.error_+1234"); given(codecException.isTransient()).willReturn(true); given(codecException.isRecoverable()).willReturn(false); String expectedFormat = "diagnosticInformation=android.media.MediaCodec.error_+1234 : isTransient=true : isRecoverable=false"; String actualFormat = ErrorFormatter.formatCodecException(codecException); assertThat(actualFormat).isEqualTo(expectedFormat); } private class IncorrectFormatThrowable extends Throwable { IncorrectFormatThrowable(String message) { super(message); } } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/exoplayer/forwarder/ExoPlayerErrorMapperTest.java ================================================ package com.novoda.noplayer.internal.exoplayer.forwarder; import android.net.Uri; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackExceptionFactory; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.audio.AudioDecoderException; import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioSink; import com.google.android.exoplayer2.drm.DecryptionException; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.KeysExpiredException; import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer; import com.google.android.exoplayer2.offline.DownloadException; import com.google.android.exoplayer2.source.ClippingMediaSource; import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.dash.DashManifestStaleException; import com.google.android.exoplayer2.source.hls.SampleQueueMappingException; import com.google.android.exoplayer2.text.SubtitleDecoderException; import com.google.android.exoplayer2.upstream.AssetDataSource; import com.google.android.exoplayer2.upstream.ContentDataSource; import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.FileDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.UdpDataSource; import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.util.PriorityTaskManager; import com.novoda.noplayer.DetailErrorType; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.PlayerErrorType; import com.novoda.noplayer.internal.exoplayer.error.ExoPlayerErrorMapper; import java.io.IOException; import java.util.Arrays; import java.util.Collection; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import static com.novoda.noplayer.DetailErrorType.ADS_LOAD_UNEXPECTED_ERROR_THEN_WILL_SKIP; import static com.novoda.noplayer.DetailErrorType.AD_GROUP_LOAD_ERROR_THEN_WILL_SKIP; import static com.novoda.noplayer.DetailErrorType.AD_LOAD_ERROR_THEN_WILL_SKIP; import static com.novoda.noplayer.DetailErrorType.ALL_ADS_LOAD_ERROR_THEN_WILL_SKIP; import static com.novoda.noplayer.DetailErrorType.AUDIO_DECODER_ERROR; import static com.novoda.noplayer.DetailErrorType.AUDIO_SINK_CONFIGURATION_ERROR; import static com.novoda.noplayer.DetailErrorType.AUDIO_SINK_INITIALISATION_ERROR; import static com.novoda.noplayer.DetailErrorType.AUDIO_SINK_WRITE_ERROR; import static com.novoda.noplayer.DetailErrorType.AUDIO_UNHANDLED_FORMAT_ERROR; import static com.novoda.noplayer.DetailErrorType.CACHE_WRITING_DATA_ERROR; import static com.novoda.noplayer.DetailErrorType.CLIPPING_MEDIA_SOURCE_CANNOT_CLIP_WRAPPED_SOURCE_INVALID_PERIOD_COUNT; import static com.novoda.noplayer.DetailErrorType.CLIPPING_MEDIA_SOURCE_CANNOT_CLIP_WRAPPED_SOURCE_NOT_SEEKABLE_TO_START; import static com.novoda.noplayer.DetailErrorType.DATA_POSITION_OUT_OF_RANGE_ERROR; import static com.novoda.noplayer.DetailErrorType.DECODING_SUBTITLE_ERROR; import static com.novoda.noplayer.DetailErrorType.DOWNLOAD_ERROR; import static com.novoda.noplayer.DetailErrorType.DRM_INSTANTIATION_ERROR; import static com.novoda.noplayer.DetailErrorType.DRM_KEYS_EXPIRED_ERROR; import static com.novoda.noplayer.DetailErrorType.DRM_SESSION_ERROR; import static com.novoda.noplayer.DetailErrorType.FAIL_DECRYPT_DATA_DUE_NON_PLATFORM_COMPONENT_ERROR; import static com.novoda.noplayer.DetailErrorType.HTTP_CANNOT_CLOSE_ERROR; import static com.novoda.noplayer.DetailErrorType.HTTP_CANNOT_OPEN_ERROR; import static com.novoda.noplayer.DetailErrorType.HTTP_CANNOT_READ_ERROR; import static com.novoda.noplayer.DetailErrorType.INITIALISATION_ERROR; import static com.novoda.noplayer.DetailErrorType.LIVE_STALE_MANIFEST_AND_NEW_MANIFEST_COULD_NOT_LOAD_ERROR; import static com.novoda.noplayer.DetailErrorType.MEDIA_REQUIRES_DRM_SESSION_MANAGER_ERROR; import static com.novoda.noplayer.DetailErrorType.MERGING_MEDIA_SOURCE_CANNOT_MERGE_ITS_SOURCES; import static com.novoda.noplayer.DetailErrorType.MULTIPLE_RENDERER_MEDIA_CLOCK_ENABLED_ERROR; import static com.novoda.noplayer.DetailErrorType.PARSING_MEDIA_DATA_OR_METADATA_ERROR; import static com.novoda.noplayer.DetailErrorType.READING_LOCAL_FILE_ERROR; import static com.novoda.noplayer.DetailErrorType.READ_CONTENT_URI_ERROR; import static com.novoda.noplayer.DetailErrorType.READ_FROM_UDP_ERROR; import static com.novoda.noplayer.DetailErrorType.READ_LOCAL_ASSET_ERROR; import static com.novoda.noplayer.DetailErrorType.SAMPLE_QUEUE_MAPPING_ERROR; import static com.novoda.noplayer.DetailErrorType.TASK_CANNOT_PROCEED_PRIORITY_TOO_LOW; import static com.novoda.noplayer.DetailErrorType.UNEXPECTED_LOADING_ERROR; import static com.novoda.noplayer.DetailErrorType.UNSUPPORTED_DRM_SCHEME_ERROR; import static com.novoda.noplayer.PlayerErrorType.CONNECTIVITY; import static com.novoda.noplayer.PlayerErrorType.CONTENT_DECRYPTION; import static com.novoda.noplayer.PlayerErrorType.DRM; import static com.novoda.noplayer.PlayerErrorType.RENDERER_DECODER; import static com.novoda.noplayer.PlayerErrorType.SOURCE; import static com.novoda.noplayer.PlayerErrorType.UNEXPECTED; import static org.fest.assertions.api.Assertions.assertThat; @RunWith(Parameterized.class) public class ExoPlayerErrorMapperTest { @Parameterized.Parameter public PlayerErrorType playerErrorType; @Parameterized.Parameter(1) public DetailErrorType detailErrorType; @Parameterized.Parameter(2) public ExoPlaybackException exoPlaybackException; @Parameterized.Parameters(name = "{0} with detail {1} is mapped from {2}") public static Collection parameters() { return Arrays.asList( new Object[]{SOURCE, SAMPLE_QUEUE_MAPPING_ERROR, createSource(new SampleQueueMappingException("mimetype-sample"))}, new Object[]{SOURCE, READING_LOCAL_FILE_ERROR, createSource(new FileDataSource.FileDataSourceException(new IOException()))}, new Object[]{SOURCE, UNEXPECTED_LOADING_ERROR, createSource(new Loader.UnexpectedLoaderException(new Throwable()))}, new Object[]{SOURCE, LIVE_STALE_MANIFEST_AND_NEW_MANIFEST_COULD_NOT_LOAD_ERROR, createSource(new DashManifestStaleException())}, new Object[]{SOURCE, DOWNLOAD_ERROR, createSource(new DownloadException("download-exception"))}, new Object[]{SOURCE, AD_LOAD_ERROR_THEN_WILL_SKIP, createSource(AdsMediaSource.AdLoadException.createForAd(new Exception()))}, new Object[]{SOURCE, AD_GROUP_LOAD_ERROR_THEN_WILL_SKIP, createSource(AdsMediaSource.AdLoadException.createForAdGroup(new Exception(), 0))}, new Object[]{SOURCE, ALL_ADS_LOAD_ERROR_THEN_WILL_SKIP, createSource(AdsMediaSource.AdLoadException.createForAllAds(new Exception()))}, new Object[]{SOURCE, ADS_LOAD_UNEXPECTED_ERROR_THEN_WILL_SKIP, createSource(AdsMediaSource.AdLoadException.createForUnexpected(new RuntimeException()))}, new Object[]{SOURCE, MERGING_MEDIA_SOURCE_CANNOT_MERGE_ITS_SOURCES, createSource(new MergingMediaSource.IllegalMergeException(MergingMediaSource.IllegalMergeException.REASON_PERIOD_COUNT_MISMATCH))}, new Object[]{SOURCE, CLIPPING_MEDIA_SOURCE_CANNOT_CLIP_WRAPPED_SOURCE_INVALID_PERIOD_COUNT, createSource(new ClippingMediaSource.IllegalClippingException(ClippingMediaSource.IllegalClippingException.REASON_INVALID_PERIOD_COUNT))}, new Object[]{SOURCE, CLIPPING_MEDIA_SOURCE_CANNOT_CLIP_WRAPPED_SOURCE_NOT_SEEKABLE_TO_START, createSource(new ClippingMediaSource.IllegalClippingException(ClippingMediaSource.IllegalClippingException.REASON_NOT_SEEKABLE_TO_START))}, new Object[]{SOURCE, CLIPPING_MEDIA_SOURCE_CANNOT_CLIP_WRAPPED_SOURCE_INVALID_PERIOD_COUNT, createSource(new ClippingMediaSource.IllegalClippingException(ClippingMediaSource.IllegalClippingException.REASON_INVALID_PERIOD_COUNT))}, new Object[]{SOURCE, TASK_CANNOT_PROCEED_PRIORITY_TOO_LOW, createSource(new PriorityTaskManager.PriorityTooLowException(1, 2))}, new Object[]{SOURCE, PARSING_MEDIA_DATA_OR_METADATA_ERROR, createSource(new ParserException())}, new Object[]{SOURCE, CACHE_WRITING_DATA_ERROR, createSource(new Cache.CacheException("cache-exception"))}, new Object[]{SOURCE, READ_LOCAL_ASSET_ERROR, createSource(new AssetDataSource.AssetDataSourceException(new IOException()))}, new Object[]{SOURCE, DATA_POSITION_OUT_OF_RANGE_ERROR, createSource(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE))}, new Object[]{CONNECTIVITY, HTTP_CANNOT_OPEN_ERROR, createSource(new HttpDataSource.HttpDataSourceException(new DataSpec(Uri.EMPTY, 0), HttpDataSource.HttpDataSourceException.TYPE_OPEN))}, new Object[]{CONNECTIVITY, HTTP_CANNOT_READ_ERROR, createSource(new HttpDataSource.HttpDataSourceException(new DataSpec(Uri.EMPTY, 0), HttpDataSource.HttpDataSourceException.TYPE_READ))}, new Object[]{CONNECTIVITY, HTTP_CANNOT_CLOSE_ERROR, createSource(new HttpDataSource.HttpDataSourceException(new DataSpec(Uri.EMPTY, 0), HttpDataSource.HttpDataSourceException.TYPE_CLOSE))}, new Object[]{CONNECTIVITY, READ_CONTENT_URI_ERROR, createSource(new ContentDataSource.ContentDataSourceException(new IOException()))}, new Object[]{CONNECTIVITY, READ_FROM_UDP_ERROR, createSource(new UdpDataSource.UdpDataSourceException(new IOException()))}, new Object[]{RENDERER_DECODER, AUDIO_SINK_CONFIGURATION_ERROR, createRenderer(new AudioSink.ConfigurationException("configuration-exception"))}, new Object[]{RENDERER_DECODER, AUDIO_SINK_INITIALISATION_ERROR, createRenderer(new AudioSink.InitializationException(0, 0, 0, 0))}, new Object[]{RENDERER_DECODER, AUDIO_SINK_WRITE_ERROR, createRenderer(new AudioSink.WriteException(0))}, new Object[]{RENDERER_DECODER, AUDIO_UNHANDLED_FORMAT_ERROR, createRenderer(new AudioProcessor.UnhandledFormatException(0, 0, 0))}, new Object[]{RENDERER_DECODER, AUDIO_DECODER_ERROR, createRenderer(new AudioDecoderException("audio-decoder-exception"))}, new Object[]{RENDERER_DECODER, INITIALISATION_ERROR, createRenderer(new MediaCodecRenderer.DecoderInitializationException(Format.createSampleFormat("id", "sample-mimety[e", 0), new Throwable(), true, 0))}, new Object[]{RENDERER_DECODER, DECODING_SUBTITLE_ERROR, createRenderer(new SubtitleDecoderException("metadata-decoder-exception"))}, new Object[]{DRM, UNSUPPORTED_DRM_SCHEME_ERROR, createRenderer(new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME))}, new Object[]{DRM, DRM_INSTANTIATION_ERROR, createRenderer(new UnsupportedDrmException(UnsupportedDrmException.REASON_INSTANTIATION_ERROR))}, new Object[]{DRM, DRM_SESSION_ERROR, createRenderer(new DrmSession.DrmSessionException(new Throwable()))}, new Object[]{DRM, DRM_KEYS_EXPIRED_ERROR, createRenderer(new KeysExpiredException())}, new Object[]{DRM, MEDIA_REQUIRES_DRM_SESSION_MANAGER_ERROR, createRenderer(new IllegalStateException())}, new Object[]{CONTENT_DECRYPTION, FAIL_DECRYPT_DATA_DUE_NON_PLATFORM_COMPONENT_ERROR, createRenderer(new DecryptionException(0, "decryption-exception"))}, new Object[]{UNEXPECTED, MULTIPLE_RENDERER_MEDIA_CLOCK_ENABLED_ERROR, ExoPlaybackExceptionFactory.createForUnexpected(new IllegalStateException("Multiple renderer media clocks enabled."))}, new Object[]{PlayerErrorType.UNKNOWN, DetailErrorType.UNKNOWN, ExoPlaybackExceptionFactory.createForUnexpected(new IllegalStateException("Any other exception"))} // DefaultAudioSink.InvalidAudioTrackTimestampException is private, cannot create // EGLSurfaceTexture.GlException is private, cannot create // PlaylistStuckException constructor is private, cannot create // PlaylistResetException constructor is private, cannot create // MediaCodecUtil.DecoderQueryException constructor is private, cannot create // DefaultDrmSessionManager.MissingSchemeDataException constructor is private, cannot create // Crypto Exceptions cannot be instantiated, it throws a RuntimeException("Stub!") ); } private static ExoPlaybackException createSource(IOException exception) { return ExoPlaybackException.createForSource(exception); } private static ExoPlaybackException createRenderer(Exception exception) { return ExoPlaybackException.createForRenderer(exception, 0); } @Test public void mapErrors() { NoPlayer.PlayerError playerError = ExoPlayerErrorMapper.errorFor(exoPlaybackException); assertThat(playerError.type()).isEqualTo(playerErrorType); assertThat(playerError.detailType()).isEqualTo(detailErrorType); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/exoplayer/mediasource/AudioFormatFixture.java ================================================ package com.novoda.noplayer.internal.exoplayer.mediasource; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.drm.DrmInitData; import java.util.Collections; import java.util.List; class AudioFormatFixture { private String id = "id"; private String sampleMimeType = "mime_type"; private String codecs = "codecs"; private int bitrate = 100; private int maxInputSize = 200; private int channelCount = 2; private int sampleRate = 50; private List initializationData = Collections.emptyList(); private DrmInitData drmInitData = new DrmInitData(Collections.emptyList()); private int selectionFlags = 0; private String language = "english"; static AudioFormatFixture anAudioFormat() { return new AudioFormatFixture(); } AudioFormatFixture withId(String id) { this.id = id; return this; } AudioFormatFixture withSampleMimeType(String sampleMimeType) { this.sampleMimeType = sampleMimeType; return this; } AudioFormatFixture withCodecs(String codecs) { this.codecs = codecs; return this; } AudioFormatFixture withBitrate(int bitrate) { this.bitrate = bitrate; return this; } AudioFormatFixture withMaxInputSize(int maxInputSize) { this.maxInputSize = maxInputSize; return this; } AudioFormatFixture withChannelCount(int channelCount) { this.channelCount = channelCount; return this; } AudioFormatFixture withSampleRate(int sampleRate) { this.sampleRate = sampleRate; return this; } AudioFormatFixture withInitializationData(List initializationData) { this.initializationData = initializationData; return this; } AudioFormatFixture withDrmInitData(DrmInitData drmInitData) { this.drmInitData = drmInitData; return this; } AudioFormatFixture withSelectionFlags(int selectionFlags) { this.selectionFlags = selectionFlags; return this; } AudioFormatFixture withLanguage(String language) { this.language = language; return this; } Format build() { return Format.createAudioSampleFormat( id, sampleMimeType, codecs, bitrate, maxInputSize, channelCount, sampleRate, initializationData, drmInitData, selectionFlags, language ); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/exoplayer/mediasource/AudioTrackTypeTest.java ================================================ package com.novoda.noplayer.internal.exoplayer.mediasource; import org.junit.Test; import static org.fest.assertions.api.Assertions.assertThat; public class AudioTrackTypeTest { private static final int MAIN_SELECTION_FLAG = 1; private static final int ALTERNATIVE_SELECTION_FLAG = 0; private static final int RANDOM_SELECTION_FLAG = 2; @Test public void givenSelectionFlagIsZero_whenCreatingAudioTrackType_thenReturnsAlternative() { AudioTrackType audioTrackType = AudioTrackType.from(ALTERNATIVE_SELECTION_FLAG); assertThat(audioTrackType).isEqualTo(AudioTrackType.ALTERNATIVE); } @Test public void givenSelectionFlagIsOne_whenCreatingAudioTrackType_thenReturnsMain() { AudioTrackType audioTrackType = AudioTrackType.from(MAIN_SELECTION_FLAG); assertThat(audioTrackType).isEqualTo(AudioTrackType.MAIN); } @Test public void givenAnyOtherSelectionFlag_whenCreatingAudioTrackType_thenReturnsUnknown() { AudioTrackType audioTrackType = AudioTrackType.from(RANDOM_SELECTION_FLAG); assertThat(audioTrackType).isEqualTo(AudioTrackType.UNKNOWN); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/exoplayer/mediasource/ExoPlayerAudioTrackSelectorTest.java ================================================ package com.novoda.noplayer.internal.exoplayer.mediasource; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.novoda.noplayer.internal.exoplayer.RendererTypeRequester; import com.novoda.noplayer.model.AudioTracks; import com.novoda.noplayer.model.PlayerAudioTrack; import java.util.Collections; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; public class ExoPlayerAudioTrackSelectorTest { private static final String ANY_TRACK_ID = "any_track_id"; private static final String ANY_LANGUAGE = "any_language"; private static final String ANY_MIME_TYPE = "any_mime_type"; private static final int ANY_NUMBER_OF_CHANNELS = 2; private static final int ANY_FREQUENCY = 50; private static final int FIRST_GROUP = 0; private static final int FIRST_TRACK = 0; private static final int SECOND_GROUP = 1; private static final int THIRD_TRACK = 2; private static final int MAIN_AUDIO_TRACK_TYPE = 1; private static final Format AUDIO_FORMAT = AudioFormatFixture.anAudioFormat().withId("id1").withSelectionFlags(MAIN_AUDIO_TRACK_TYPE).build(); @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock private ExoPlayerTrackSelector trackSelector; @Mock private RendererTypeRequester rendererTypeRequester; private ExoPlayerAudioTrackSelector exoPlayerAudioTrackSelector; private static final PlayerAudioTrack AUDIO_TRACK = new PlayerAudioTrack(SECOND_GROUP, THIRD_TRACK, ANY_TRACK_ID, ANY_LANGUAGE, ANY_MIME_TYPE, ANY_NUMBER_OF_CHANNELS, ANY_FREQUENCY, AudioTrackType.MAIN); @Before public void setUp() { exoPlayerAudioTrackSelector = new ExoPlayerAudioTrackSelector(trackSelector); } @Test public void givenTrackSelectorContainsTracks_whenSelectingAudioTrack_thenSelectsTrack() { TrackGroupArray trackGroups = givenTrackSelectorContainsTracks(); ArgumentCaptor argumentCaptor = whenSelectingAudioTrack(trackGroups); DefaultTrackSelector.SelectionOverride selectionOverride = argumentCaptor.getValue(); assertThat(selectionOverride.groupIndex).isEqualTo(SECOND_GROUP); assertThat(selectionOverride.tracks).contains(THIRD_TRACK); } @Test public void givenTrackSelectorContainsUnsupportedTracks_whenGettingAudioTracks_thenReturnsOnlySupportedTracks() { givenTrackSelectorContainsUnsupportedTracks(); AudioTracks actualAudioTracks = exoPlayerAudioTrackSelector.getAudioTracks(rendererTypeRequester); assertThat(actualAudioTracks).isEqualTo(expectedSupportedAudioTracks()); } private TrackGroupArray givenTrackSelectorContainsTracks() { TrackGroupArray trackGroups = new TrackGroupArray( new TrackGroup(AudioFormatFixture.anAudioFormat().build()), new TrackGroup( AudioFormatFixture.anAudioFormat().build(), AudioFormatFixture.anAudioFormat().build(), AudioFormatFixture.anAudioFormat().build() ) ); given(trackSelector.trackGroups(TrackType.AUDIO, rendererTypeRequester)).willReturn(trackGroups); return trackGroups; } private ArgumentCaptor whenSelectingAudioTrack(TrackGroupArray trackGroups) { exoPlayerAudioTrackSelector.selectAudioTrack(AUDIO_TRACK, rendererTypeRequester); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(DefaultTrackSelector.SelectionOverride.class); verify(trackSelector).setSelectionOverride(eq(TrackType.AUDIO), any(RendererTypeRequester.class), eq(trackGroups), argumentCaptor.capture()); return argumentCaptor; } private void givenTrackSelectorContainsUnsupportedTracks() { TrackGroupArray trackGroups = new TrackGroupArray( new TrackGroup(AUDIO_FORMAT), new TrackGroup( AudioFormatFixture.anAudioFormat().build(), AudioFormatFixture.anAudioFormat().build(), AudioFormatFixture.anAudioFormat().build() ) ); given(trackSelector.trackGroups(TrackType.AUDIO, rendererTypeRequester)).willReturn(trackGroups); given(trackSelector.supportsTrackSwitching(eq(TrackType.AUDIO), any(RendererTypeRequester.class), any(TrackGroupArray.class), anyInt())) .willReturn(true) .willReturn(false); } private AudioTracks expectedSupportedAudioTracks() { return AudioTracks.from( Collections.singletonList( new PlayerAudioTrack( FIRST_GROUP, FIRST_TRACK, AUDIO_FORMAT.id, AUDIO_FORMAT.language, AUDIO_FORMAT.sampleMimeType, AUDIO_FORMAT.channelCount, AUDIO_FORMAT.bitrate, AudioTrackType.from(AUDIO_FORMAT.selectionFlags) ) ) ); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/exoplayer/mediasource/ExoPlayerVideoTrackSelectorTest.java ================================================ package com.novoda.noplayer.internal.exoplayer.mediasource; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.novoda.noplayer.ContentType; import com.novoda.noplayer.internal.exoplayer.RendererTypeRequester; import com.novoda.noplayer.internal.utils.Optional; import com.novoda.noplayer.model.PlayerVideoTrack; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import java.util.Arrays; import java.util.List; import static com.novoda.noplayer.internal.exoplayer.mediasource.VideoFormatFixture.aVideoFormat; import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; public class ExoPlayerVideoTrackSelectorTest { private static final Format VIDEO_FORMAT = aVideoFormat().withId("id1").build(); private static final PlayerVideoTrack PLAYER_VIDEO_TRACK = new PlayerVideoTrack( 0, 0, VIDEO_FORMAT.id, ContentType.HLS, VIDEO_FORMAT.width, VIDEO_FORMAT.height, (int) VIDEO_FORMAT.frameRate, VIDEO_FORMAT.bitrate ); private static final Format ADDITIONAL_VIDEO_FORMAT = aVideoFormat().withId("id2").build(); private static final int FIRST_GROUP = 0; private static final int SECOND_TRACK = 1; private static final PlayerVideoTrack ADDITIONAL_PLAYER_VIDEO_TRACK = new PlayerVideoTrack( FIRST_GROUP, SECOND_TRACK, ADDITIONAL_VIDEO_FORMAT.id, ContentType.HLS, ADDITIONAL_VIDEO_FORMAT.width, ADDITIONAL_VIDEO_FORMAT.height, (int) ADDITIONAL_VIDEO_FORMAT.frameRate, ADDITIONAL_VIDEO_FORMAT.bitrate ); private static final List EXPECTED_TRACKS = Arrays.asList(PLAYER_VIDEO_TRACK, ADDITIONAL_PLAYER_VIDEO_TRACK); @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock private ExoPlayerTrackSelector trackSelector; @Mock private TrackSelection.Factory trackSelectionFactory; @Mock private RendererTypeRequester rendererTypeRequester; @Mock private SimpleExoPlayer exoPlayer; private ExoPlayerVideoTrackSelector exoPlayerVideoTrackSelector; @Before public void setUp() { exoPlayerVideoTrackSelector = new ExoPlayerVideoTrackSelector(trackSelector); } @Test public void givenTrackSelectorContainsTracks_whenSelectingVideoTrack_thenSelectsTrack() { givenTrackSelectorContainsTracks(); ArgumentCaptor argumentCaptor = whenSelectingVideoTrack(ADDITIONAL_PLAYER_VIDEO_TRACK); DefaultTrackSelector.SelectionOverride selectionOverride = argumentCaptor.getValue(); assertThat(selectionOverride.groupIndex).isEqualTo(FIRST_GROUP); assertThat(selectionOverride.tracks).contains(SECOND_TRACK); } @Test public void givenTrackSelector_whenGettingVideoTracks_thenReturnsSupportedTracks() { givenTrackSelectorContainsTracks(); List actualVideoTracks = exoPlayerVideoTrackSelector.getVideoTracks(rendererTypeRequester, ContentType.HLS); assertThat(actualVideoTracks).isEqualTo(EXPECTED_TRACKS); } @Test public void givenTrackSelector_whenGettingCurrentlySelectedVideoTrack_thenReturnsSelectedTrack() { givenTrackSelectorContainsTracks(); given(exoPlayer.getVideoFormat()).willReturn(ADDITIONAL_VIDEO_FORMAT); Optional selectedVideoTrack = exoPlayerVideoTrackSelector.getSelectedVideoTrack(exoPlayer, rendererTypeRequester, ContentType.HLS); assertThat(selectedVideoTrack).isEqualTo(Optional.of(ADDITIONAL_PLAYER_VIDEO_TRACK)); } @Test public void givenNoCurrentlySelectedTrack_whenGettingCurrentlySelectedVideoTrack_thenReturnsAbsent() { givenTrackSelectorContainsTracks(); given(exoPlayer.getVideoFormat()).willReturn(null); Optional selectedVideoTrack = exoPlayerVideoTrackSelector.getSelectedVideoTrack(exoPlayer, rendererTypeRequester, ContentType.HLS); assertThat(selectedVideoTrack).isEqualTo(Optional.absent()); } @Test public void givenTrackSelector_whenClearMaxVideoBitrate_thenClearsMaxVideoBitrate() { givenTrackSelectorContainsTracks(); exoPlayerVideoTrackSelector.clearMaxVideoBitrate(); verify(trackSelector).clearMaxVideoBitrate(); } @Test public void givenTrackSelector_whenSetMaxVideoBitrate1000000_thenSetsMaxVideoBitrate1000000() { givenTrackSelectorContainsTracks(); exoPlayerVideoTrackSelector.setMaxVideoBitrate(1000000); verify(trackSelector).setMaxVideoBitrate(1000000); } private void givenTrackSelectorContainsTracks() { TrackGroupArray trackGroups = new TrackGroupArray( new TrackGroup(VIDEO_FORMAT, ADDITIONAL_VIDEO_FORMAT) ); given(trackSelector.trackGroups(TrackType.VIDEO, rendererTypeRequester)).willReturn(trackGroups); } private ArgumentCaptor whenSelectingVideoTrack(PlayerVideoTrack videoTrack) { exoPlayerVideoTrackSelector.selectVideoTrack(videoTrack, rendererTypeRequester); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(DefaultTrackSelector.SelectionOverride.class); verify(trackSelector).setSelectionOverride(eq(TrackType.VIDEO), any(RendererTypeRequester.class), any(TrackGroupArray.class), argumentCaptor.capture()); return argumentCaptor; } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/exoplayer/mediasource/RendererTrackIndexExtractorTest.java ================================================ package com.novoda.noplayer.internal.exoplayer.mediasource; import com.google.android.exoplayer2.C; import com.novoda.noplayer.internal.exoplayer.RendererTypeRequester; import com.novoda.noplayer.internal.utils.Optional; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import static org.fest.assertions.api.Assertions.assertThat; public class RendererTrackIndexExtractorTest { @Rule public MockitoRule rule = MockitoJUnit.rule(); private RendererTrackIndexExtractor extractor; @Before public void setUp() throws Exception { extractor = new RendererTrackIndexExtractor(); } @Test public void givenAudioTrackAtPositionZero_whenExtractingAudioIndex_thenReturnsIndexZero() { Optional audioIndex = extractor.extract(TrackType.AUDIO, 1, rendererTypeRequesterAudioTrack); int expectedAudioIndex = 0; assertThat(audioIndex.get()).isEqualTo(expectedAudioIndex); } @Test public void givenVideoTrackAtPositionZero_whenExtractingVideoIndex_thenReturnsIndexZero() { Optional videoIndex = extractor.extract(TrackType.VIDEO, 1, rendererTypeRequesterVideoTrack); int expectedVideoIndex = 0; assertThat(videoIndex.get()).isEqualTo(expectedVideoIndex); } @Test public void givenSubtitlesTrackAtPositionZero_whenExtractingTextIndex_thenReturnsIndexZero() { Optional textIndex = extractor.extract(TrackType.TEXT, 1, rendererTypeRequesterTextTrack); int expectedTextIndex = 0; assertThat(textIndex.get()).isEqualTo(expectedTextIndex); } @Test public void givenThreeTrackTypes_whenExtractingAudioIndexes_thenReturnsIndexOne() { Optional audioIndex = extractor.extract(TrackType.AUDIO, 3, rendererTypeRequesterVideoAudioTextTrack); int expectedAudioIndex = 1; assertThat(audioIndex.get()).isEqualTo(expectedAudioIndex); } @Test public void givenNoAudioTrack_whenExtractingAudioIndex_thenReturnsEmpty() { Optional audioIndex = extractor.extract(TrackType.AUDIO, 1, emptyRendererTypeRequester); assertThat(audioIndex.isAbsent()).isTrue(); } @Test public void givenNoVideoTrack_whenExtractingVideoIndex_thenReturnsEmpty() { Optional videoIndex = extractor.extract(TrackType.VIDEO, 1, emptyRendererTypeRequester); assertThat(videoIndex.isAbsent()).isTrue(); } @Test public void givenNoTextTrack_whenExtractingTextIndex_thenReturnsEmpty() { Optional textIndex = extractor.extract(TrackType.TEXT, 1, emptyRendererTypeRequester); assertThat(textIndex.isAbsent()).isTrue(); } private RendererTypeRequester rendererTypeRequesterAudioTrack = new RendererTypeRequester() { @Override public int getRendererTypeFor(int index) { if (index == 0) { return C.TRACK_TYPE_AUDIO; } return -1; } }; private RendererTypeRequester rendererTypeRequesterVideoTrack = new RendererTypeRequester() { @Override public int getRendererTypeFor(int index) { if (index == 0) { return C.TRACK_TYPE_VIDEO; } return -1; } }; private RendererTypeRequester rendererTypeRequesterTextTrack = new RendererTypeRequester() { @Override public int getRendererTypeFor(int index) { if (index == 0) { return C.TRACK_TYPE_TEXT; } return -1; } }; private RendererTypeRequester emptyRendererTypeRequester = new RendererTypeRequester() { @Override public int getRendererTypeFor(int index) { return -1; } }; private RendererTypeRequester rendererTypeRequesterVideoAudioTextTrack = new RendererTypeRequester() { @Override public int getRendererTypeFor(int index) { switch (index) { case 0: return C.TRACK_TYPE_VIDEO; case 1: return C.TRACK_TYPE_AUDIO; case 2: return C.TRACK_TYPE_TEXT; default: return -1; } } }; } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/exoplayer/mediasource/VideoFormatFixture.java ================================================ package com.novoda.noplayer.internal.exoplayer.mediasource; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.drm.DrmInitData; import java.util.Collections; import java.util.List; public class VideoFormatFixture { private String id = "id"; private String sampleMimeType = "mime_type"; private String codecs = "codecs"; private int bitrate = 100; private int maxInputSize = 200; private int width = 1920; private int height = 1080; private float frameRate; private List initializationData = Collections.emptyList(); private DrmInitData drmInitData = new DrmInitData(Collections.emptyList()); public static VideoFormatFixture aVideoFormat() { return new VideoFormatFixture(); } private VideoFormatFixture() { // Uses static factory method. } public VideoFormatFixture withId(String id) { this.id = id; return this; } public VideoFormatFixture withSampleMimeType(String sampleMimeType) { this.sampleMimeType = sampleMimeType; return this; } public VideoFormatFixture withCodecs(String codecs) { this.codecs = codecs; return this; } public VideoFormatFixture withBitrate(int bitrate) { this.bitrate = bitrate; return this; } public VideoFormatFixture withMaxInputSize(int maxInputSize) { this.maxInputSize = maxInputSize; return this; } public VideoFormatFixture withWidth(int width) { this.width = width; return this; } public VideoFormatFixture withHeight(int height) { this.height = height; return this; } public VideoFormatFixture withFrameRate(float frameRate) { this.frameRate = frameRate; return this; } public VideoFormatFixture withInitializationData(List initializationData) { this.initializationData = initializationData; return this; } public VideoFormatFixture withDrmInitData(DrmInitData drmInitData) { this.drmInitData = drmInitData; return this; } public Format build() { return Format.createVideoSampleFormat( id, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, initializationData, drmInitData ); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/listeners/BufferStateListenersTest.java ================================================ package com.novoda.noplayer.internal.listeners; import com.novoda.noplayer.NoPlayer; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import static org.mockito.Mockito.verify; public class BufferStateListenersTest { @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock private NoPlayer.BufferStateListener aBufferStateListener; @Mock private NoPlayer.BufferStateListener anotherBufferStateListener; private BufferStateListeners bufferStateListeners; @Before public void setUp() { bufferStateListeners = new BufferStateListeners(); bufferStateListeners.add(aBufferStateListener); bufferStateListeners.add(anotherBufferStateListener); } @Test public void givenBufferStateListeners_whenNotifyingOfBufferStarted_thenAllTheListenersAreNotifiedAppropriately() { bufferStateListeners.onBufferStarted(); verify(aBufferStateListener).onBufferStarted(); verify(anotherBufferStateListener).onBufferStarted(); } @Test public void givenBufferStateListeners_whenNotifyingOfBufferCompleted_thenAllTheListenersAreNotifiedAppropriately() { bufferStateListeners.onBufferCompleted(); verify(aBufferStateListener).onBufferCompleted(); verify(anotherBufferStateListener).onBufferCompleted(); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/listeners/CompletionListenersTest.java ================================================ package com.novoda.noplayer.internal.listeners; import com.novoda.noplayer.NoPlayer; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; public class CompletionListenersTest { @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock private NoPlayer.CompletionListener completionListener; private CompletionListeners completionListeners; @Before public void setUp() { completionListeners = new CompletionListeners(); completionListeners.add(completionListener); } @Test public void whenCallingOnCompletion_thenNotifiesOnCompletion() { completionListeners.onCompletion(); verify(completionListener).onCompletion(); } @Test public void whenCallingOnCompletionTwice_thenDoesNothing() { completionListeners.onCompletion(); reset(completionListener); completionListeners.onCompletion(); verify(completionListener, never()).onCompletion(); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/listeners/StateChangedListenersTest.java ================================================ package com.novoda.noplayer.internal.listeners; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.internal.utils.NoPlayerLog; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import static org.mockito.Mockito.verify; public class StateChangedListenersTest { @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock private NoPlayer.StateChangedListener stateChangedListener; private StateChangedListeners stateChangedListeners; @Before public void setUp() { NoPlayerLog.setLoggingEnabled(false); stateChangedListeners = new StateChangedListeners(); stateChangedListeners.add(stateChangedListener); } @Test public void whenDoubleCallingOnVideoPlaying_thenEmitsOnlyFirstOnVideoPlayingEvent() { stateChangedListeners.onVideoPlaying(); stateChangedListeners.onVideoPlaying(); verify(stateChangedListener).onVideoPlaying(); } @Test public void whenDoubleCallingOnVideoPaused_thenEmitsOnlyFirstOnVideoPausedEvent() { stateChangedListeners.onVideoPaused(); stateChangedListeners.onVideoPaused(); verify(stateChangedListener).onVideoPaused(); } @Test public void whenDoubleCallingOnVideoStopped_thenEmitsOnlyFirstOnVideoStoppedEvent() { stateChangedListeners.onVideoStopped(); stateChangedListeners.onVideoStopped(); verify(stateChangedListener).onVideoStopped(); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/mediaplayer/AndroidMediaPlayerAudioTrackSelectorTest.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import android.media.MediaPlayer; import com.novoda.noplayer.internal.exoplayer.mediasource.AudioTrackType; import com.novoda.noplayer.model.AudioTracks; import com.novoda.noplayer.model.PlayerAudioTrack; import java.util.Arrays; import java.util.Collections; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import utils.ExceptionMatcher; import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static utils.ExceptionMatcher.matches; public class AndroidMediaPlayerAudioTrackSelectorTest { @Rule public ExpectedException thrown = ExpectedException.none(); @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); private static final int NO_FORMAT = 0; private static final int NO_CHANNELS = -1; private static final int NO_FREQUENCY = -1; private static final int AUDIO_TRACK_INDEX = 2; private static final String NO_MIME_TYPE = ""; private static final String ANY_LANGUAGE = "english"; private static final NoPlayerTrackInfo AUDIO_TRACK_INFO = mock(NoPlayerTrackInfo.class); private static final NoPlayerTrackInfo VIDEO_TRACK_INFO = mock(NoPlayerTrackInfo.class); private static final NoPlayerTrackInfo UNKNOWN_TRACK_INFO = mock(NoPlayerTrackInfo.class); @Mock private TrackInfosFactory trackInfosFactory; @Mock private MediaPlayer mediaPlayer; @Mock private PlayerAudioTrack playerAudioTrack; private AndroidMediaPlayerAudioTrackSelector trackSelector; @Before public void setUp() { trackSelector = new AndroidMediaPlayerAudioTrackSelector(trackInfosFactory); } @Test public void givenNullMediaPlayer_whenGettingAudioTracks_thenThrowsIllegalState() { thrown.expect(ExceptionMatcher.matches("You can only call getAudioTracks() when video is prepared.", IllegalStateException.class)); trackSelector.getAudioTracks(null); } @Test public void givenTrackSelectorContainsUnsupportedTracks_whenGettingAudioTracks_thenReturnsOnlySupportedTracks() { givenTrackSelectorContainsUnsupportedTracks(); AudioTracks audioTracks = trackSelector.getAudioTracks(mediaPlayer); assertThat(audioTracks).isEqualTo(expectedAudioTrack()); } @Test public void givenNullMediaPlayer_whenSelectingAudioTrack_thenThrowsIllegalState() { thrown.expect(matches("You can only call selectAudioTrack() when video is prepared.", IllegalStateException.class)); trackSelector.selectAudioTrack(null, mock(PlayerAudioTrack.class)); } @Test public void whenSelectingAudioTrack_thenMediaPlayerSelectsAudioTrack() { PlayerAudioTrack playerAudioTrack = mock(PlayerAudioTrack.class); given(playerAudioTrack.groupIndex()).willReturn(AUDIO_TRACK_INDEX); trackSelector.selectAudioTrack(mediaPlayer, playerAudioTrack); verify(mediaPlayer).selectTrack(AUDIO_TRACK_INDEX); } private void givenTrackSelectorContainsUnsupportedTracks() { given(AUDIO_TRACK_INFO.type()).willReturn(MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO); given(AUDIO_TRACK_INFO.language()).willReturn(ANY_LANGUAGE); given(VIDEO_TRACK_INFO.type()).willReturn(MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_VIDEO); given(VIDEO_TRACK_INFO.language()).willReturn(ANY_LANGUAGE); given(UNKNOWN_TRACK_INFO.type()).willReturn(MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_UNKNOWN); given(UNKNOWN_TRACK_INFO.language()).willReturn(ANY_LANGUAGE); NoPlayerTrackInfos noPlayerTrackInfos = new NoPlayerTrackInfos( Arrays.asList( VIDEO_TRACK_INFO, UNKNOWN_TRACK_INFO, AUDIO_TRACK_INFO ) ); given(trackInfosFactory.createFrom(mediaPlayer)).willReturn(noPlayerTrackInfos); } private AudioTracks expectedAudioTrack() { return AudioTracks.from( Collections.singletonList( new PlayerAudioTrack( AUDIO_TRACK_INDEX, NO_FORMAT, String.valueOf(AUDIO_TRACK_INFO.hashCode()), AUDIO_TRACK_INFO.language(), NO_MIME_TYPE, NO_CHANNELS, NO_FREQUENCY, AudioTrackType.MAIN ) ) ); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/mediaplayer/AndroidMediaPlayerFacadeTest.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import android.content.Context; import android.media.AudioManager; import android.media.MediaPlayer; import android.net.Uri; import android.view.Surface; import android.view.SurfaceHolder; import com.novoda.noplayer.SurfaceRequester; import com.novoda.noplayer.internal.mediaplayer.forwarder.MediaPlayerForwarder; import com.novoda.noplayer.internal.utils.NoPlayerLog; import com.novoda.noplayer.model.AudioTracks; import com.novoda.noplayer.model.Either; import com.novoda.noplayer.model.PlayerAudioTrack; import com.novoda.noplayer.model.PlayerAudioTrackFixture; import com.novoda.noplayer.model.PlayerSubtitleTrack; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.mockito.stubbing.Answer; import utils.ExceptionMatcher; import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Map; import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; public class AndroidMediaPlayerFacadeTest { private static final int ANY_DURATION = 12000; private static final int ANY_POSITION = 60; private static final int ANY_WIDTH = 100; private static final int ANY_HEIGHT = 50; private static final int ANY_ERROR_WHAT = -1; private static final int ANY_ERROR_EXTRA = 404; private static final int TEN_PERCENT = 10; private static final int TEN_SECONDS_IN_MILLIS = 10000; private static final float ANY_VOLUME = 0.5f; private static final boolean SCREEN_ON = true; private static final boolean IS_IN_PLAYBACK_STATE = true; private static final boolean IS_NOT_IN_PLAYBACK_STATE = false; private static final boolean IS_PLAYING = true; private static final boolean IS_NOT_PLAYING = false; private static final Map NO_HEADERS = null; private static final Uri ANY_URI = mock(Uri.class); private static final PlayerAudioTrack PLAYER_AUDIO_TRACK = PlayerAudioTrackFixture.aPlayerAudioTrack().build(); private static final AudioTracks AUDIO_TRACKS = AudioTracks.from(Collections.singletonList(PLAYER_AUDIO_TRACK)); private static final String ERROR_MESSAGE = "Video must be loaded and not in an error state before trying to interact with the player"; @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Rule public ExpectedException thrown = ExpectedException.none(); @Mock private Context context; @Mock private AndroidMediaPlayerAudioTrackSelector trackSelector; @Mock private PlaybackStateChecker playbackStateChecker; @Mock private MediaPlayerCreator mediaPlayerCreator; @Mock private MediaPlayer mediaPlayer; @Mock private AudioManager audioManager; @Mock private SurfaceRequester surfaceRequester; @Mock private Surface surface; @Mock private MediaPlayer.OnPreparedListener preparedListener; @Mock private MediaPlayer.OnVideoSizeChangedListener videoSizeChangedListener; @Mock private MediaPlayer.OnErrorListener errorListener; @Mock private MediaPlayer.OnCompletionListener completionListener; @Mock private MediaPlayerForwarder forwarder; private Either eitherSurface; private AndroidMediaPlayerFacade facade; @Before public void setUp() { NoPlayerLog.setLoggingEnabled(false); facade = new AndroidMediaPlayerFacade(context, forwarder, audioManager, trackSelector, playbackStateChecker, mediaPlayerCreator); given(mediaPlayerCreator.createMediaPlayer()).willReturn(mediaPlayer); given(playbackStateChecker.isInPlaybackState(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class))).willReturn(IS_IN_PLAYBACK_STATE); eitherSurface = Either.left(surface); givenSurfaceRequesterReturns(eitherSurface); given(forwarder.onPreparedListener()).willReturn(preparedListener); given(forwarder.onCompletionListener()).willReturn(completionListener); given(forwarder.onErrorListener()).willReturn(errorListener); given(forwarder.onSizeChangedListener()).willReturn(videoSizeChangedListener); } private void givenSurfaceRequesterReturns(final Either surface) { doAnswer(new Answer() { @Override public Void answer(InvocationOnMock invocation) { SurfaceRequester.Callback callback = invocation.getArgument(0); callback.onSurfaceReady(surface); return null; } }).when(surfaceRequester).requestSurface(any(SurfaceRequester.Callback.class)); } @Test public void whenPreparing_thenRequestsAudioFocus() { givenMediaPlayerIsPrepared(); verify(audioManager).requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); } @Test public void whenPreparing_thenDoesNotReleaseMediaPlayer() { givenMediaPlayerIsPrepared(); verify(mediaPlayer, never()).reset(); verify(mediaPlayer, never()).release(); } @Test public void whenPreparingMultipleTimes_thenReleasesMediaPlayer() { facade.prepareVideo(ANY_URI, eitherSurface); facade.prepareVideo(ANY_URI, eitherSurface); verify(mediaPlayer).reset(); verify(mediaPlayer).release(); } @Test public void whenPreparing_thenSetsDataSource() throws IOException { givenMediaPlayerIsPrepared(); verify(mediaPlayer).setDataSource(context, ANY_URI, NO_HEADERS); } @Test public void givenSurfaceRequesterReturnsSurface_whenPreparing_thenSetsSurface() { Surface surface = mock(Surface.class); Either eitherSurface = Either.left(surface); givenSurfaceRequesterReturns(eitherSurface); givenMediaPlayerIsPreparedWith(eitherSurface); verify(mediaPlayer).setSurface(surface); } @Test public void givenSurfaceRequesterReturnsSurfaceHolder_whenPreparing_thenSetsDisplay() { SurfaceHolder surfaceHolder = mock(SurfaceHolder.class); Either eitherSurface = Either.right(surfaceHolder); givenSurfaceRequesterReturns(eitherSurface); givenMediaPlayerIsPreparedWith(eitherSurface); verify(mediaPlayer).setDisplay(surfaceHolder); } @Test public void whenPreparing_thenSetsStreamMusicAudioStreamType() { givenMediaPlayerIsPrepared(); verify(mediaPlayer).setAudioStreamType(AudioManager.STREAM_MUSIC); } @Test public void whenPreparing_thenSetsScreenOnWhilePlayerToTrue() { givenMediaPlayerIsPrepared(); verify(mediaPlayer).setScreenOnWhilePlaying(SCREEN_ON); } @Test public void whenPreparing_thenPreparesMediaPlayerAsynchronously() { givenMediaPlayerIsPrepared(); verify(mediaPlayer).prepareAsync(); } @Test public void givenExceptionPreparingMediaPlayer_whenPreparingMediaPlayer_thenForwardsOnError() { doAnswer(new Answer() { @Override public Void answer(InvocationOnMock invocation) { throw new IllegalStateException("cannot prepare async"); } }).when(mediaPlayer).prepareAsync(); givenMediaPlayerIsPrepared(); whenErroring(); verify(errorListener).onError(mediaPlayer, ANY_ERROR_WHAT, ANY_ERROR_EXTRA); } @Test public void givenBoundPreparedListener_andMediaPlayerIsPrepared_whenPrepared_thenForwardsOnPrepared() { facade.prepareVideo(ANY_URI, eitherSurface); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(MediaPlayer.OnPreparedListener.class); verify(mediaPlayer).setOnPreparedListener(argumentCaptor.capture()); argumentCaptor.getValue().onPrepared(mediaPlayer); verify(preparedListener).onPrepared(mediaPlayer); } @Test public void givenNoBoundPreparedListener_andMediaPlayerIsPrepared_whenPrepared_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches("Should bind a OnPreparedListener. Cannot forward events.", IllegalStateException.class)); given(forwarder.onPreparedListener()).willReturn(null); givenMediaPlayerIsPrepared(); } @Test public void givenBoundVideoSizeChangedListener_andMediaPlayerOnPrepared_whenVideoSizeChanges_thenForwardsSizeChanges() { givenMediaPlayerIsPrepared(); whenVideoSizeChanges(); verify(videoSizeChangedListener).onVideoSizeChanged(eq(mediaPlayer), eq(ANY_WIDTH), eq(ANY_HEIGHT)); } @Test public void givenNoBoundVideoSizeChangedListener_andMediaPlayerIsPrepared_whenVideoSizeChanges_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches("Should bind a OnVideoSizeChangedListener. Cannot forward events.", IllegalStateException.class)); given(forwarder.onSizeChangedListener()).willReturn(null); givenMediaPlayerIsPrepared(); whenVideoSizeChanges(); } @Test public void givenBoundCompletionListener_andMediaPlayerIsPrepared_whenCompleted_thenForwardsCompleted() { givenMediaPlayerIsPrepared(); whenCompleted(); verify(completionListener).onCompletion(mediaPlayer); } @Test public void givenNoBoundCompletionListener_andMediaPlayerIsPrepared_whenCompleted_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches("Should bind a OnCompletionListener. Cannot forward events.", IllegalStateException.class)); given(forwarder.onCompletionListener()).willReturn(null); givenMediaPlayerIsPrepared(); whenCompleted(); } @Test public void givenBoundErrorListener_andMediaPlayerIsPrepared_whenErroring_thenForwardsError() { givenMediaPlayerIsPrepared(); whenErroring(); verify(errorListener).onError(mediaPlayer, ANY_ERROR_WHAT, ANY_ERROR_EXTRA); } @Test public void givenNoBoundErrorListener_andMediaPlayerIsPrepared_whenErroring_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches("Should bind a OnErrorListener. Cannot forward events.", IllegalStateException.class)); given(forwarder.onErrorListener()).willReturn(null); givenMediaPlayerIsPrepared(); whenErroring(); } @Test public void givenBoundBufferListener_andMediaPlayerIsPrepared_whenBuffering_thenBufferPercentageIsUpdated() { givenMediaPlayerIsPrepared(); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(MediaPlayer.OnBufferingUpdateListener.class); verify(mediaPlayer).setOnBufferingUpdateListener(argumentCaptor.capture()); argumentCaptor.getValue().onBufferingUpdate(mediaPlayer, TEN_PERCENT); int bufferPercentage = facade.getBufferPercentage(); assertThat(bufferPercentage).isEqualTo(TEN_PERCENT); } @Test public void givenMediaPlayerIsPrepared_whenReleasing_thenReleasesMediaPlayer() { givenMediaPlayerIsPrepared(); facade.release(); verify(mediaPlayer).reset(); verify(mediaPlayer).release(); } @Test public void givenMediaPlayerIsPreparedWithSurface_whenStarting_thenSetsSurface() { givenMediaPlayerIsPrepared(); reset(mediaPlayer); facade.start(eitherSurface); verify(mediaPlayer).setSurface(surface); } @Test public void givenMediaPlayerIsPreparedWithSurfaceHolder_whenStarting_thenSetsDisplay() { SurfaceHolder surfaceHolder = mock(SurfaceHolder.class); Either eitherSurface = Either.right(surfaceHolder); givenMediaPlayerIsPreparedWith(eitherSurface); reset(mediaPlayer); facade.start(eitherSurface); verify(mediaPlayer).setDisplay(surfaceHolder); } @Test public void givenMediaPlayerIsNotPrepared_whenStarting_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class)); facade.start(eitherSurface); } @Test public void givenMediaPlayerIsPrepared_whenStarting_thenStartsMediaPlayer() { givenMediaPlayerIsPrepared(); facade.start(eitherSurface); verify(mediaPlayer).start(); } @Test public void givenMediaPlayerIsPlaying_whenPausing_thenPausesMediaPlayer() { givenMediaPlayerIsPrepared(); given(playbackStateChecker.isPlaying(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class))) .willReturn(IS_PLAYING); facade.pause(); verify(mediaPlayer).pause(); } @Test public void givenMediaPlayerIsNotPlaying_whenPausing_thenDoesNotPausesMediaPlayer() { givenMediaPlayerIsPrepared(); given(playbackStateChecker.isPlaying(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class))) .willReturn(IS_NOT_PLAYING); facade.pause(); verify(mediaPlayer, never()).pause(); } @Test public void givenMediaPlayerIsNotInPlaybackState_whenPausing_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class)); given(playbackStateChecker.isInPlaybackState(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class))) .willReturn(IS_NOT_IN_PLAYBACK_STATE); facade.pause(); verify(mediaPlayer).pause(); } @Test public void givenMediaPlayerIsInPlaybackState_whenGettingDuration_thenReturnsDuration() { givenMediaPlayerIsPrepared(); given(playbackStateChecker.isInPlaybackState(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class))) .willReturn(IS_IN_PLAYBACK_STATE); given(mediaPlayer.getDuration()).willReturn(ANY_DURATION); int duration = facade.mediaDurationInMillis(); assertThat(duration).isEqualTo(ANY_DURATION); } @Test public void givenMediaPlayerIsNotInPlaybackState_whenGettingDuration_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class)); given(playbackStateChecker.isInPlaybackState(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class))) .willReturn(IS_NOT_IN_PLAYBACK_STATE); facade.mediaDurationInMillis(); } @Test public void givenMediaPlayerIsInPlaybackState_whenGettingPosition_thenReturnsPosition() { givenMediaPlayerIsPrepared(); given(playbackStateChecker.isInPlaybackState(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class))) .willReturn(IS_IN_PLAYBACK_STATE); given(mediaPlayer.getCurrentPosition()).willReturn(ANY_POSITION); int currentPosition = facade.currentPositionInMillis(); assertThat(currentPosition).isEqualTo(ANY_POSITION); } @Test public void givenMediaPlayerIsNotInPlaybackState_whenGettingPosition_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class)); given(playbackStateChecker.isInPlaybackState(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class))) .willReturn(IS_NOT_IN_PLAYBACK_STATE); facade.currentPositionInMillis(); } @Test public void givenMediaPlayerIsInPlaybackState_whenSeeking_thenSeeksToPosition() { givenMediaPlayerIsPrepared(); given(playbackStateChecker.isInPlaybackState(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class))) .willReturn(IS_IN_PLAYBACK_STATE); facade.seekTo(TEN_SECONDS_IN_MILLIS); verify(mediaPlayer).seekTo(TEN_SECONDS_IN_MILLIS); } @Test public void givenMediaPlayerIsNotInPlaybackState_whenSeeking_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class)); given(playbackStateChecker.isInPlaybackState(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class))) .willReturn(IS_NOT_IN_PLAYBACK_STATE); facade.seekTo(TEN_SECONDS_IN_MILLIS); } @Test public void whenCheckingIsPlaying_thenDelegatesToPlaystateChecker() { givenMediaPlayerIsPrepared(); given(playbackStateChecker.isPlaying(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class))) .willReturn(IS_PLAYING); boolean playing = facade.isPlaying(); assertThat(playing).isTrue(); } @Test public void givenNoMediaPlayer_whenGettingBufferPercentage_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class)); facade.getBufferPercentage(); } @Test public void givenMediaPlayerIsNotInPlaybackState_whenGettingAudioTracks_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class)); given(playbackStateChecker.isPlaying(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class))) .willReturn(IS_PLAYING); facade.getAudioTracks(); } @Test public void whenGettingAudioTracks_thenDelegatesToTrackSelector() { givenMediaPlayerIsPrepared(); given(trackSelector.getAudioTracks(mediaPlayer)).willReturn(AUDIO_TRACKS); AudioTracks audioTracks = facade.getAudioTracks(); assertThat(audioTracks).isEqualTo(AUDIO_TRACKS); } @Test public void givenMediaPlayerIsNotInPlaybackState_whenSelectingAudioTracks_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class)); given(playbackStateChecker.isPlaying(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class))) .willReturn(IS_PLAYING); facade.selectAudioTrack(mock(PlayerAudioTrack.class)); } @Test public void whenSelectingAudioTrack_thenDelegatesToTrackSelector() { givenMediaPlayerIsPrepared(); PlayerAudioTrack audioTrack = mock(PlayerAudioTrack.class); facade.selectAudioTrack(audioTrack); verify(trackSelector).selectAudioTrack(mediaPlayer, audioTrack); } @Test public void givenMediaPlayerIsNotInPlaybackState_whenSelectingSubtitleTrack_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class)); given(playbackStateChecker.isPlaying(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class))) .willReturn(IS_PLAYING); facade.selectSubtitleTrack(mock(PlayerSubtitleTrack.class)); } @Test public void givenMediaPlayerIsNotInPlaybackState_whenClearingSubtitleTrack_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class)); given(playbackStateChecker.isInPlaybackState(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class))) .willReturn(IS_NOT_IN_PLAYBACK_STATE); facade.clearSubtitleTrack(); } @Test public void givenMediaPlayerIsNotInPlaybackState_whenGettingSubtitleTracks_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class)); given(playbackStateChecker.isInPlaybackState(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class))) .willReturn(IS_NOT_IN_PLAYBACK_STATE); facade.getSubtitleTracks(); } @Test public void whenGettingSubtitleTracks_thenReturnsEmptyList() { givenMediaPlayerIsPrepared(); List subtitleTracks = facade.getSubtitleTracks(); assertThat(subtitleTracks).isEmpty(); } @Test public void whenSelectingSubtitleTrack_thenReturnsFalse() { givenMediaPlayerIsPrepared(); boolean result = facade.selectSubtitleTrack(mock(PlayerSubtitleTrack.class)); assertThat(result).isFalse(); } @Test public void whenSettingOnSeekCompleteListener_thenSetsOnSeekCompleteListener() { givenMediaPlayerIsPrepared(); MediaPlayer.OnSeekCompleteListener onSeekCompleteListener = mock(MediaPlayer.OnSeekCompleteListener.class); facade.setOnSeekCompleteListener(onSeekCompleteListener); verify(mediaPlayer).setOnSeekCompleteListener(onSeekCompleteListener); } @Test public void givenMediaPlayerIsNotInPlaybackState_whenSettingOnSeekCompleteListener_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class)); given(playbackStateChecker.isInPlaybackState(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class))) .willReturn(IS_NOT_IN_PLAYBACK_STATE); facade.setOnSeekCompleteListener(mock(MediaPlayer.OnSeekCompleteListener.class)); } @Test public void givenMediaPlayerIsNotInPlaybackState_whenGettingVideoTracks_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class)); given(playbackStateChecker.isPlaying(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class))) .willReturn(IS_PLAYING); facade.getVideoTracks(); } @Test public void givenMediaPlayerIsNotInPlaybackState_whenGettingSelectedVideoTrack_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class)); given(playbackStateChecker.isPlaying(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class))) .willReturn(IS_PLAYING); facade.getSelectedVideoTrack(); } @Test public void givenMediaPlayerIsNotInPlaybackState_whenSettingVolume_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class)); facade.setVolume(ANY_VOLUME); } @Test public void whenSettingVolume_thenSetsLeftAndRightVolumeScalars() { givenMediaPlayerIsPrepared(); facade.setVolume(ANY_VOLUME); verify(mediaPlayer).setVolume(ANY_VOLUME, ANY_VOLUME); } @Test public void givenMediaPlayerIsNotInPlaybackState_whenGettingVolume_thenThrowsIllegalStateException() { thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class)); facade.getVolume(); } @Test public void givenNoVolumeWasSet_whenGettingVolume_theReturnsOne() { givenMediaPlayerIsPrepared(); float currentVolume = facade.getVolume(); assertThat(currentVolume).isEqualTo(1f); } @Test public void givenVolumeWasSet_whenGettingVolume_theReturnsSetVolume() { givenMediaPlayerIsPrepared(); facade.setVolume(ANY_VOLUME); float currentVolume = facade.getVolume(); assertThat(currentVolume).isEqualTo(ANY_VOLUME); } private void givenMediaPlayerIsPrepared() { givenMediaPlayerIsPreparedWith(eitherSurface); } private void givenMediaPlayerIsPreparedWith(Either eitherSurface) { facade.prepareVideo(ANY_URI, eitherSurface); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(MediaPlayer.OnPreparedListener.class); verify(mediaPlayer).setOnPreparedListener(argumentCaptor.capture()); argumentCaptor.getValue().onPrepared(mediaPlayer); } private void whenVideoSizeChanges() { ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(MediaPlayer.OnVideoSizeChangedListener.class); verify(mediaPlayer).setOnVideoSizeChangedListener(argumentCaptor.capture()); argumentCaptor.getValue().onVideoSizeChanged(mediaPlayer, ANY_WIDTH, ANY_HEIGHT); } private void whenCompleted() { ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(MediaPlayer.OnCompletionListener.class); verify(mediaPlayer).setOnCompletionListener(argumentCaptor.capture()); argumentCaptor.getValue().onCompletion(mediaPlayer); } private void whenErroring() { ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(MediaPlayer.OnErrorListener.class); verify(mediaPlayer).setOnErrorListener(argumentCaptor.capture()); argumentCaptor.getValue().onError(mediaPlayer, ANY_ERROR_WHAT, ANY_ERROR_EXTRA); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/mediaplayer/AndroidMediaPlayerImplTest.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import android.media.MediaPlayer; import android.net.Uri; import android.view.Surface; import android.view.SurfaceHolder; import android.view.View; import com.novoda.noplayer.ContentType; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.Options; import com.novoda.noplayer.OptionsBuilder; import com.novoda.noplayer.PlayerInformation; import com.novoda.noplayer.PlayerSurfaceHolder; import com.novoda.noplayer.PlayerView; import com.novoda.noplayer.SurfaceRequester; import com.novoda.noplayer.internal.Heart; import com.novoda.noplayer.internal.listeners.PlayerListenersHolder; import com.novoda.noplayer.internal.mediaplayer.forwarder.MediaPlayerForwarder; import com.novoda.noplayer.internal.utils.NoPlayerLog; import com.novoda.noplayer.model.AudioTracks; import com.novoda.noplayer.model.Either; import com.novoda.noplayer.model.LoadTimeout; import com.novoda.noplayer.model.PlayerAudioTrack; import com.novoda.noplayer.model.PlayerAudioTrackFixture; import com.novoda.noplayer.model.Timeout; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.experimental.runners.Enclosed; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.mockito.stubbing.Answer; import java.util.Collections; import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @RunWith(Enclosed.class) public class AndroidMediaPlayerImplTest { public static class GivenPlayer extends Base { private static final boolean IS_BEATING = true; private static final boolean IS_NOT_BEATING = false; private static final int WIDTH = 100; private static final int HEIGHT = 200; private static final int ANY_ROTATION_DEGREES = 0; private static final int ANY_PIXEL_WIDTH_HEIGHT = 1; private static final long TWO_MINUTES_IN_MILLIS = 120000; private static final int ONE_SECOND_IN_MILLIS = 1000; private static final boolean IS_PLAYING = true; private static final PlayerAudioTrack PLAYER_AUDIO_TRACK = PlayerAudioTrackFixture.aPlayerAudioTrack().build(); private static final AudioTracks AUDIO_TRACKS = AudioTracks.from(Collections.singletonList(PLAYER_AUDIO_TRACK)); @Test public void whenInitialising_thenBindsListenersToForwarder() { player.initialise(); verify(forwarder).bind(preparedListener, player); verify(forwarder).bind(bufferStateListener, errorListener); verify(forwarder).bind(completionListener, stateChangedListener); verify(forwarder).bind(videoSizeChangedListener); verify(forwarder).bind(infoListener); } @Test public void whenInitialising_thenBindsListenerToBufferHeartbeatCallback() { player.initialise(); verify(checkBufferHeartbeatCallback).bind(bufferListener); } @Test public void whenInitialising_thenBindsHeartbeatCallbackToListenerHolder() { player.initialise(); verify(listenersHolder).addHeartbeatCallback(checkBufferHeartbeatCallback); } @Test public void givenInitialised_whenCallingOnPrepared_thenCancelsTimeout() { player.initialise(); ArgumentCaptor preparedListenerCaptor = ArgumentCaptor.forClass(NoPlayer.PreparedListener.class); verify(listenersHolder).addPreparedListener(preparedListenerCaptor.capture()); NoPlayer.PreparedListener preparedListener = preparedListenerCaptor.getValue(); preparedListener.onPrepared(player); verify(loadTimeout).cancel(); } @Test public void whenInitialising_thenBindsHeart() { player.initialise(); verify(heart).bind(any(Heart.Heartbeat.class)); } @Test public void givenInitialised_whenCallingOnPrepared_thenSetsOnSeekCompleteListener() { player.initialise(); ArgumentCaptor preparedListenerCaptor = ArgumentCaptor.forClass(NoPlayer.PreparedListener.class); verify(listenersHolder).addPreparedListener(preparedListenerCaptor.capture()); NoPlayer.PreparedListener preparedListener = preparedListenerCaptor.getValue(); preparedListener.onPrepared(player); verify(mediaPlayer).setOnSeekCompleteListener(any(MediaPlayer.OnSeekCompleteListener.class)); } @Test public void givenInitialised_whenCallingOnError_thenCancelsTimeout() { player.initialise(); ArgumentCaptor errorListenerCaptor = ArgumentCaptor.forClass(NoPlayer.ErrorListener.class); verify(listenersHolder).addErrorListener(errorListenerCaptor.capture()); NoPlayer.ErrorListener errorListener = errorListenerCaptor.getValue(); errorListener.onError(mock(NoPlayer.PlayerError.class)); verify(loadTimeout).cancel(); } @Test public void givenInitialised_whenCallingOnError_thenPlayerResourcesAreReleased_andNotListeners() { player.initialise(); ArgumentCaptor errorListenerCaptor = ArgumentCaptor.forClass(NoPlayer.ErrorListener.class); verify(listenersHolder).addErrorListener(errorListenerCaptor.capture()); NoPlayer.ErrorListener errorListener = errorListenerCaptor.getValue(); errorListener.onError(mock(NoPlayer.PlayerError.class)); verify(delayedActionExecutor).clearAllActions(); verify(listenersHolder).resetState(); verify(loadTimeout).cancel(); verify(heart).stopBeatingHeart(); verify(mediaPlayer).release(); verify(listenersHolder, never()).clear(); verify(stateChangedListener, never()).onVideoStopped(); } @Test public void givenInitialised_whenCallingOnVideoSizeChanged_thenVideoWidthAndHeightMatches() { player.initialise(); ArgumentCaptor videoSizeChangedListenerCaptor = ArgumentCaptor.forClass(NoPlayer.VideoSizeChangedListener.class); verify(listenersHolder).addVideoSizeChangedListener(videoSizeChangedListenerCaptor.capture()); NoPlayer.VideoSizeChangedListener videoSizeChangedListener = videoSizeChangedListenerCaptor.getValue(); videoSizeChangedListener.onVideoSizeChanged(WIDTH, HEIGHT, ANY_ROTATION_DEGREES, ANY_PIXEL_WIDTH_HEIGHT); assertThat(player.videoWidth()).isEqualTo(WIDTH); assertThat(player.videoHeight()).isEqualTo(HEIGHT); } @Test public void givenAndroidMediaPlayerIsPlaying_whenCallingIsPlaying_thenReturnsTrue() { given(mediaPlayer.isPlaying()).willReturn(IS_PLAYING); boolean isPlaying = player.isPlaying(); assertThat(isPlaying).isTrue(); } @Test public void whenSeeking_thenSeeksToPosition() { long seekPositionInMillis = TWO_MINUTES_IN_MILLIS; player.seekTo(seekPositionInMillis); verify(mediaPlayer).seekTo(seekPositionInMillis); } @Test public void whenPausing_thenPausesMediaPlayer() { player.pause(); verify(mediaPlayer).pause(); } @Test public void givenHeartIsBeating_whenPausing_thenStopsBeatingHeart() { given(heart.isBeating()).willReturn(IS_BEATING); player.pause(); verify(heart).stopBeatingHeart(); } @Test public void givenHeartIsBeating_whenPausing_thenForcesHeartBeat() { given(heart.isBeating()).willReturn(IS_BEATING); player.pause(); verify(heart).forceBeat(); } @Test public void givenHeartIsNotBeating_whenPausing_thenDoesNotStopBeatingHeart() { given(heart.isBeating()).willReturn(IS_NOT_BEATING); player.pause(); verify(heart, never()).stopBeatingHeart(); } @Test public void givenHeartIsNotBeating_whenPausing_thenDoesNotForceHeartBeat() { given(heart.isBeating()).willReturn(IS_NOT_BEATING); player.pause(); verify(heart, never()).forceBeat(); } @Test public void whenPausing_thenNotifiesStateChangedListenersThatVideoIsPaused() { player.pause(); verify(stateChangedListener).onVideoPaused(); } @Test public void givenPlayerIsNotSeeking_whenGettingPlayheadPosition_thenReturnsCurrentMediaPlayerPosition() { given(mediaPlayer.currentPositionInMillis()).willReturn(ONE_SECOND_IN_MILLIS); long playheadPositionInMillis = player.playheadPositionInMillis(); assertThat(playheadPositionInMillis).isEqualTo(ONE_SECOND_IN_MILLIS); } @Test public void givenPlayerIsSeeking_whenGettingPlayheadPosition_thenReturnsSeekPosition() { long seekPositionInMillis = TEN_SECONDS; player.seekTo(seekPositionInMillis); long videoPositionInMillis = player.playheadPositionInMillis(); assertThat(videoPositionInMillis).isEqualTo(seekPositionInMillis); } @Test public void whenGettingMediaDuration_thenReturnsMediaPlayerDuration() { given(mediaPlayer.mediaDurationInMillis()).willReturn(ONE_SECOND_IN_MILLIS); long videoDurationInMillis = player.mediaDurationInMillis(); assertThat(videoDurationInMillis).isEqualTo(ONE_SECOND_IN_MILLIS); } @Test public void whenGettingBufferPercentage_thenReturnsMediaPlayerBufferPercentage() { int mediaPlayerBufferPercentage = 10; given(mediaPlayer.getBufferPercentage()).willReturn(mediaPlayerBufferPercentage); int bufferPercentage = player.bufferPercentage(); assertThat(bufferPercentage).isEqualTo(mediaPlayerBufferPercentage); } @Test public void whenGettingPlayerInformation_thenReturnsMediaPlayerInformation() { PlayerInformation playerInformation = player.getPlayerInformation(); assertThat(playerInformation).isEqualTo(mediaPlayerInformation); } @Test public void whenAttachingPlayerView_thenAddsVideoSizeChangedListener() { NoPlayer.VideoSizeChangedListener videoSizeChangedListener = mock(NoPlayer.VideoSizeChangedListener.class); given(playerView.getVideoSizeChangedListener()).willReturn(videoSizeChangedListener); player.attach(playerView); verify(listenersHolder).addVideoSizeChangedListener(videoSizeChangedListener); } @Test public void whenAttachingPlayerView_thenAddsStateChangedListener() { NoPlayer.StateChangedListener stateChangedListener = mock(NoPlayer.StateChangedListener.class); given(playerView.getStateChangedListener()).willReturn(stateChangedListener); player.attach(playerView); verify(listenersHolder).addStateChangedListener(stateChangedListener); } @Test public void whenAttachingPlayerView_thenPreventsVideoDriverBug() { player.attach(playerView); verify(buggyVideoDriverPreventer).preventVideoDriverBug(player, containerView); } @Test public void whenDetachingPlayerView_thenRemovesVideoSizeChangedListener() { PlayerView playerView = mock(PlayerView.class); NoPlayer.VideoSizeChangedListener videoSizeChangedListener = mock(NoPlayer.VideoSizeChangedListener.class); given(playerView.getVideoSizeChangedListener()).willReturn(videoSizeChangedListener); player.detach(playerView); verify(listenersHolder).removeVideoSizeChangedListener(videoSizeChangedListener); } @Test public void whenDetachingPlayerView_thenRemovesStateChangedListener() { PlayerView playerView = mock(PlayerView.class); NoPlayer.StateChangedListener stateChangedListener = mock(NoPlayer.StateChangedListener.class); given(playerView.getStateChangedListener()).willReturn(stateChangedListener); player.detach(playerView); verify(listenersHolder).removeStateChangedListener(stateChangedListener); } @Test public void whenDetachingPlayerView_thenClearsVideoDriverBugPreventer() { PlayerView playerView = mock(PlayerView.class); View containerView = mock(View.class); given(playerView.getContainerView()).willReturn(containerView); player.detach(playerView); verify(buggyVideoDriverPreventer).clear(containerView); } @Test public void whenSelectingAudioTrack_thenDelegatesToMediaPlayer() { PlayerAudioTrack audioTrack = mock(PlayerAudioTrack.class); player.selectAudioTrack(audioTrack); verify(mediaPlayer).selectAudioTrack(audioTrack); } @Test public void whenGettingAudioTracks_thenDelegatesToMediaPlayer() { given(mediaPlayer.getAudioTracks()).willReturn(AUDIO_TRACKS); AudioTracks audioTracks = player.getAudioTracks(); assertThat(audioTracks).isEqualTo(AUDIO_TRACKS); } @Test public void whenStopping_thenPlayerResourcesAreReleased_andNotListeners() { player.stop(); verify(delayedActionExecutor).clearAllActions(); verify(listenersHolder).resetState(); verify(stateChangedListener).onVideoStopped(); verify(loadTimeout).cancel(); verify(heart).stopBeatingHeart(); verify(mediaPlayer).release(); verify(listenersHolder, never()).clear(); } @Test public void whenReleasing_thenPlayerResourcesAreReleased() { player.release(); verify(delayedActionExecutor).clearAllActions(); verify(listenersHolder).resetState(); verify(stateChangedListener).onVideoStopped(); verify(loadTimeout).cancel(); verify(heart).stopBeatingHeart(); verify(mediaPlayer).release(); verify(listenersHolder).clear(); } @Test public void givenAttachedPlayerView_whenStopping_thenPlayerResourcesAreReleased_andNotListeners() { player.attach(playerView); player.stop(); verify(delayedActionExecutor).clearAllActions(); verify(listenersHolder).resetState(); verify(stateChangedListener).onVideoStopped(); verify(loadTimeout).cancel(); verify(heart).stopBeatingHeart(); verify(mediaPlayer).release(); verify(containerView).setVisibility(View.GONE); verify(listenersHolder, never()).clear(); } @Test public void givenAttachedPlayerView_whenReleasing_thenPlayerResourcesAreReleased() { player.attach(playerView); player.release(); verify(delayedActionExecutor).clearAllActions(); verify(listenersHolder).resetState(); verify(stateChangedListener).onVideoStopped(); verify(loadTimeout).cancel(); verify(heart).stopBeatingHeart(); verify(mediaPlayer).release(); verify(containerView).setVisibility(View.GONE); verify(listenersHolder).clear(); } } public static class GivenPlayerIsAttached extends Base { private static final long DELAY_MILLIS = 500; private static final boolean IS_NOT_PLAYING = false; private static final float ANY_VOLUME = 0.4f; @Override public void setUp() { super.setUp(); player.attach(playerView); } @Test public void whenLoadingVideo_thenNotifiesBufferStateListenersThatBufferStarted() { player.loadVideo(URI, OPTIONS); verify(bufferStateListener).onBufferStarted(); } @Test public void whenLoadingVideo_thenPreparesVideo() { player.loadVideo(URI, OPTIONS); verify(mediaPlayer).prepareVideo(URI, surface); } @Test public void whenLoadingVideoWithTimeout_thenNotifiesBufferStateListenersThatBufferStarted() { player.loadVideoWithTimeout(URI, OPTIONS, ANY_TIMEOUT, ANY_LOAD_TIMEOUT_CALLBACK); verify(bufferStateListener).onBufferStarted(); } @Test public void whenLoadingVideoWithTimeout_thenPreparesVideo() { player.loadVideoWithTimeout(URI, OPTIONS, ANY_TIMEOUT, ANY_LOAD_TIMEOUT_CALLBACK); verify(mediaPlayer).prepareVideo(URI, surface); } @Test public void whenLoadingVideoWithTimeout_thenStartsTimeout() { player.loadVideoWithTimeout(URI, OPTIONS, ANY_TIMEOUT, ANY_LOAD_TIMEOUT_CALLBACK); verify(loadTimeout).start(ANY_TIMEOUT, ANY_LOAD_TIMEOUT_CALLBACK); } @Test public void whenLoadingVideo_thenShowsContainerView() { player.loadVideo(URI, OPTIONS); verify(containerView).setVisibility(View.VISIBLE); } @Test public void whenStartingPlay_thenStartsBeatingHeart() { player.play(); verify(heart).startBeatingHeart(); } @Test public void whenStartingPlay_thenMediaPlayerStarts() { player.play(); verify(mediaPlayer).start(surface); } @Test public void whenStartingPlay_thenNotifiesStateListenersThatVideoIsPlaying() { player.play(); verify(stateChangedListener).onVideoPlaying(); } @Test public void whenStartingPlayAtVideoPosition_thenStartsBeatingHeart() { given(mediaPlayer.currentPositionInMillis()).willReturn((int) BEGINNING_POSITION); player.playAt(BEGINNING_POSITION); verify(heart).startBeatingHeart(); } @Test public void whenStartingPlayAtVideoPosition_thenMediaPlayerStarts() { given(mediaPlayer.currentPositionInMillis()).willReturn((int) BEGINNING_POSITION); player.playAt(BEGINNING_POSITION); verify(mediaPlayer).start(surface); } @Test public void whenStartingPlayAtVideoPosition_thenNotifiesStateListenersThatVideoIsPlaying() { given(mediaPlayer.currentPositionInMillis()).willReturn((int) BEGINNING_POSITION); player.playAt(BEGINNING_POSITION); verify(stateChangedListener).onVideoPlaying(); } @Test public void givenPlayerHasPlayedVideo_whenLoadingVideo_thenPlayerIsReleased_andNotListeners() { given(mediaPlayer.hasPlayedContent()).willReturn(true); player.loadVideo(URI, OPTIONS); verify(stateChangedListener).onVideoStopped(); verify(loadTimeout).cancel(); verify(heart).stopBeatingHeart(); verify(mediaPlayer).release(); verify(listenersHolder, never()).clear(); } @Test public void givenPlayerHasPlayedVideo_whenLoadingVideoWithTimeout_thenPlayerResourcesAreReleased_andNotListeners() { given(mediaPlayer.hasPlayedContent()).willReturn(true); player.loadVideoWithTimeout(URI, OPTIONS, ANY_TIMEOUT, ANY_LOAD_TIMEOUT_CALLBACK); verify(stateChangedListener).onVideoStopped(); verify(loadTimeout).cancel(); verify(heart).stopBeatingHeart(); verify(mediaPlayer).release(); verify(listenersHolder, never()).clear(); } @Test public void givenPlayerHasNotPlayedVideo_whenLoadingVideo_thenPlayerResourcesAreNotReleased() { given(mediaPlayer.hasPlayedContent()).willReturn(false); player.loadVideo(URI, OPTIONS); verify(stateChangedListener, never()).onVideoStopped(); verify(loadTimeout, never()).cancel(); verify(heart, never()).stopBeatingHeart(); verify(mediaPlayer, never()).release(); } @Test public void givenPlayerHasNotPlayedVideo_whenLoadingVideoWithTimeout_thenPlayerResourcesAreNotReleased() { given(mediaPlayer.hasPlayedContent()).willReturn(false); player.loadVideoWithTimeout(URI, OPTIONS, ANY_TIMEOUT, ANY_LOAD_TIMEOUT_CALLBACK); verify(stateChangedListener, never()).onVideoStopped(); verify(loadTimeout, never()).cancel(); verify(heart, never()).stopBeatingHeart(); verify(mediaPlayer, never()).release(); } @Test public void givenPositionThatDiffersFromPlayheadPosition_whenStartingPlayAtVideoPosition_thenNotifiesBufferStateListenersThatBufferStarted() { long differentPositionInMillis = givenPositionThatDiffersFromPlayheadPosition(); player.playAt(differentPositionInMillis); verify(bufferStateListener).onBufferStarted(); } @Test public void givenPositionThatDiffersFromPlayheadPosition_whenStartingPlayAtVideoPosition_thenInitialisesPlaybackForSeeking() { long differentPositionInMillis = givenPositionThatDiffersFromPlayheadPosition(); player.playAt(differentPositionInMillis); thenInitialisesPlaybackForSeeking(); } @Test public void givenPositionThatDiffersFromPlayheadPosition_whenStartingPlayAtVideoPosition_thenSeeksToVideoPosition() { long differentPositionInMillis = givenPositionThatDiffersFromPlayheadPosition(); player.playAt(differentPositionInMillis); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(DelayedActionExecutor.Action.class); verify(delayedActionExecutor).performAfterDelay(argumentCaptor.capture(), eq(DELAY_MILLIS)); argumentCaptor.getValue().perform(); verify(mediaPlayer).seekTo(differentPositionInMillis); } @Test public void givenPlayerIsAlreadyPlaying_whenPlaying_thenNotifiesVideoPlaying() { given(mediaPlayer.isPlaying()).willReturn(IS_NOT_PLAYING); player.play(); verify(stateChangedListener).onVideoPlaying(); } @Test public void whenSetRepeating_thenSetRepeating() { player.setRepeating(false); verify(mediaPlayer).setRepeating(false); } @Test public void whenSetVolume_thenSetVolumeOnMediaPlayer() { player.setVolume(ANY_VOLUME); verify(mediaPlayer).setVolume(ANY_VOLUME); } @Test public void whenGetVolume_thenReturnMediaPlayerVolume() { given(mediaPlayer.getVolume()).willReturn(ANY_VOLUME); float currentVolume = player.getVolume(); assertThat(currentVolume).isEqualTo(ANY_VOLUME); } private long givenPositionThatDiffersFromPlayheadPosition() { given(mediaPlayer.currentPositionInMillis()).willReturn(0); return 1; } private void thenInitialisesPlaybackForSeeking() { InOrder inOrder = inOrder(mediaPlayer); inOrder.verify(mediaPlayer).start(surface); inOrder.verify(mediaPlayer).pause(); inOrder.verifyNoMoreInteractions(); } } public abstract static class Base { static final Options OPTIONS = new OptionsBuilder().withContentType(ContentType.H264).build(); static final long BEGINNING_POSITION = 0; static final Uri URI = Mockito.mock(Uri.class); static final int TEN_SECONDS = 10; static final Timeout ANY_TIMEOUT = Timeout.fromSeconds(TEN_SECONDS); static final NoPlayer.LoadTimeoutCallback ANY_LOAD_TIMEOUT_CALLBACK = new NoPlayer.LoadTimeoutCallback() { @Override public void onLoadTimeout() { } }; @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock MediaPlayerInformation mediaPlayerInformation; @Mock AndroidMediaPlayerFacade mediaPlayer; @Mock MediaPlayerForwarder forwarder; @Mock PlayerListenersHolder listenersHolder; @Mock CheckBufferHeartbeatCallback checkBufferHeartbeatCallback; @Mock LoadTimeout loadTimeout; @Mock Heart heart; @Mock DelayedActionExecutor delayedActionExecutor; @Mock BuggyVideoDriverPreventer buggyVideoDriverPreventer; @Mock NoPlayer.PreparedListener preparedListener; @Mock NoPlayer.BufferStateListener bufferStateListener; @Mock NoPlayer.ErrorListener errorListener; @Mock NoPlayer.CompletionListener completionListener; @Mock NoPlayer.VideoSizeChangedListener videoSizeChangedListener; @Mock NoPlayer.InfoListener infoListener; @Mock NoPlayer.StateChangedListener stateChangedListener; @Mock Either surface; @Mock PlayerView playerView; @Mock NoPlayer.StateChangedListener stateChangeListener; @Mock MediaPlayer.OnPreparedListener onPreparedListener; @Mock MediaPlayer.OnCompletionListener onCompletionListener; @Mock MediaPlayer.OnErrorListener onErrorListener; @Mock MediaPlayer.OnVideoSizeChangedListener onSizeChangedListener; @Mock CheckBufferHeartbeatCallback.BufferListener bufferListener; @Mock View containerView; @Mock PlayerSurfaceHolder playerSurfaceHolder; AndroidMediaPlayerImpl player; @Before public void setUp() { NoPlayerLog.setLoggingEnabled(false); SurfaceRequester surfaceRequester = mock(SurfaceRequester.class); given(playerView.getPlayerSurfaceHolder()).willReturn(playerSurfaceHolder); given(playerSurfaceHolder.getSurfaceRequester()).willReturn(surfaceRequester); given(playerView.getStateChangedListener()).willReturn(stateChangeListener); given(playerView.getVideoSizeChangedListener()).willReturn(videoSizeChangedListener); given(playerView.getContainerView()).willReturn(containerView); doAnswer(new Answer() { @Override public Void answer(InvocationOnMock invocation) { SurfaceRequester.Callback callback = invocation.getArgument(0); callback.onSurfaceReady(surface); return null; } }).when(surfaceRequester).requestSurface(any(SurfaceRequester.Callback.class)); given(listenersHolder.getPreparedListeners()).willReturn(preparedListener); given(listenersHolder.getBufferStateListeners()).willReturn(bufferStateListener); given(listenersHolder.getErrorListeners()).willReturn(errorListener); given(listenersHolder.getCompletionListeners()).willReturn(completionListener); given(listenersHolder.getVideoSizeChangedListeners()).willReturn(videoSizeChangedListener); given(listenersHolder.getInfoListeners()).willReturn(infoListener); given(listenersHolder.getStateChangedListeners()).willReturn(stateChangedListener); given(forwarder.onPreparedListener()).willReturn(onPreparedListener); given(forwarder.onCompletionListener()).willReturn(onCompletionListener); given(forwarder.onErrorListener()).willReturn(onErrorListener); given(forwarder.onSizeChangedListener()).willReturn(onSizeChangedListener); given(forwarder.onHeartbeatListener()).willReturn(bufferListener); player = new AndroidMediaPlayerImpl( mediaPlayerInformation, mediaPlayer, forwarder, listenersHolder, checkBufferHeartbeatCallback, loadTimeout, heart, delayedActionExecutor, buggyVideoDriverPreventer ); } } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/mediaplayer/BuggyVideoDriverPreventerTest.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import android.view.View; import com.novoda.noplayer.NoPlayer; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import static org.mockito.Mockito.*; public class BuggyVideoDriverPreventerTest { @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock private View videoContainer; @Mock private NoPlayer player; @Mock private MediaPlayerTypeReader mediaPlayerTypeReader; private BuggyVideoDriverPreventer buggyVideoDriverPreventer; @Before public void setUp() { buggyVideoDriverPreventer = new BuggyVideoDriverPreventer(mediaPlayerTypeReader); } @Test public void givenVideoDriverIsNotBuggy_whenPreventingVideoDriverBug_thenNothingHappens() { when(mediaPlayerTypeReader.getPlayerType()).thenReturn(AndroidMediaPlayerType.NU); buggyVideoDriverPreventer.preventVideoDriverBug(player, videoContainer); verifyZeroInteractions(videoContainer); } @Test public void givenVideoDriverCanBeBuggy_whenPreventingVideoDriverBug_thenABuggyDriverLayoutListenerIsAddedToTheVideoContainer() { when(mediaPlayerTypeReader.getPlayerType()).thenReturn(AndroidMediaPlayerType.AWESOME); buggyVideoDriverPreventer.preventVideoDriverBug(player, videoContainer); verify(videoContainer).addOnLayoutChangeListener(any(OnPotentialBuggyDriverLayoutListener.class)); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/mediaplayer/DelayedActionExecutorTest.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import android.os.Handler; import java.util.HashMap; import java.util.Map; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.willAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class DelayedActionExecutorTest { private static final long ANY_DELAY_IN_MILLIS = 10; private final DelayedActionExecutor.Action action = mock(DelayedActionExecutor.Action.class); private final DelayedActionExecutor.Action secondaryAction = mock(DelayedActionExecutor.Action.class); private final Handler immediatelyExecutingHandler = createImmediatelyExecutingHandler(); private final Handler nonExecutingHandler = mock(Handler.class); private final Map runnables = new HashMap<>(); private DelayedActionExecutor delayedActionExecutor; @Test public void whenActionIsNotPerformedYet_thenMapContainsAction() { delayedActionExecutor = new DelayedActionExecutor(nonExecutingHandler, runnables); delayedActionExecutor.performAfterDelay(action, ANY_DELAY_IN_MILLIS); assertThat(runnables).hasSize(1); } @Test public void whenPerformingActionAfterDelay_thenRemovesActionFromMap() { delayedActionExecutor = new DelayedActionExecutor(immediatelyExecutingHandler, runnables); delayedActionExecutor.performAfterDelay(action, ANY_DELAY_IN_MILLIS); assertThat(runnables).isEmpty(); } @Test public void whenPerformingActionAfterDelay_thenPerformsAction() { delayedActionExecutor = new DelayedActionExecutor(immediatelyExecutingHandler, runnables); delayedActionExecutor.performAfterDelay(action, ANY_DELAY_IN_MILLIS); verify(action).perform(); } @Test public void givenMultipleQueuedActions_whenClearingActions_thenRemovesAllActions() { delayedActionExecutor = new DelayedActionExecutor(nonExecutingHandler, runnables); delayedActionExecutor.performAfterDelay(action, ANY_DELAY_IN_MILLIS); delayedActionExecutor.performAfterDelay(secondaryAction, ANY_DELAY_IN_MILLIS); delayedActionExecutor.clearAllActions(); assertThat(runnables).isEmpty(); verify(nonExecutingHandler, times(2)).removeCallbacks(any(Runnable.class)); } private Handler createImmediatelyExecutingHandler() { Handler handler = mock(Handler.class); final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Runnable.class); willAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { argumentCaptor.getValue().run(); return null; } }).given(handler).postDelayed(argumentCaptor.capture(), eq(ANY_DELAY_IN_MILLIS)); return handler; } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/mediaplayer/ErrorFactoryTest.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import android.media.MediaPlayer; import com.novoda.noplayer.DetailErrorType; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.PlayerErrorType; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import java.util.Arrays; import java.util.Collection; import static com.novoda.noplayer.DetailErrorType.*; import static com.novoda.noplayer.PlayerErrorType.*; import static org.fest.assertions.api.Assertions.assertThat; @RunWith(Parameterized.class) public class ErrorFactoryTest { @Parameterized.Parameter(0) public PlayerErrorType playerErrorType; @Parameterized.Parameter(1) public DetailErrorType detailErrorType; @Parameterized.Parameter(2) public int type; @Parameterized.Parameters(name = "{0} with detail {1} is mapped from {2}") public static Collection parameters() { return Arrays.asList( new Object[]{SOURCE, MEDIA_PLAYER_MALFORMED, MediaPlayer.MEDIA_ERROR_MALFORMED}, new Object[]{SOURCE, MEDIA_PLAYER_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK, MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK}, new Object[]{SOURCE, MEDIA_PLAYER_INFO_NOT_SEEKABLE, MediaPlayer.MEDIA_INFO_NOT_SEEKABLE}, new Object[]{SOURCE, MEDIA_PLAYER_SUBTITLE_TIMED_OUT, MediaPlayer.MEDIA_INFO_SUBTITLE_TIMED_OUT}, new Object[]{SOURCE, MEDIA_PLAYER_UNSUPPORTED_SUBTITLE, MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE}, new Object[]{CONNECTIVITY, MEDIA_PLAYER_TIMED_OUT, MediaPlayer.MEDIA_ERROR_TIMED_OUT}, new Object[]{DRM, MEDIA_PLAYER_SERVER_DIED, MediaPlayer.MEDIA_ERROR_SERVER_DIED}, new Object[]{DRM, MEDIA_PLAYER_PREPARE_DRM_STATUS_PREPARATION_ERROR, MediaPlayer.PREPARE_DRM_STATUS_PREPARATION_ERROR}, new Object[]{DRM, MEDIA_PLAYER_PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR, MediaPlayer.PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR}, new Object[]{DRM, MEDIA_PLAYER_PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR, MediaPlayer.PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR}, new Object[]{RENDERER_DECODER, MEDIA_PLAYER_INFO_AUDIO_NOT_PLAYING, MediaPlayer.MEDIA_INFO_AUDIO_NOT_PLAYING}, new Object[]{RENDERER_DECODER, MEDIA_PLAYER_BAD_INTERLEAVING, MediaPlayer.MEDIA_INFO_BAD_INTERLEAVING}, new Object[]{RENDERER_DECODER, MEDIA_PLAYER_INFO_VIDEO_NOT_PLAYING, MediaPlayer.MEDIA_INFO_VIDEO_NOT_PLAYING}, new Object[]{RENDERER_DECODER, MEDIA_PLAYER_INFO_VIDEO_TRACK_LAGGING, MediaPlayer.MEDIA_INFO_VIDEO_TRACK_LAGGING} ); } @Test public void mapErrors() { NoPlayer.PlayerError playerError = ErrorFactory.createErrorFrom(type, 0); assertThat(playerError.type()).isEqualTo(playerErrorType); assertThat(playerError.detailType()).isEqualTo(detailErrorType); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/mediaplayer/ErrorFormatterTest.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import org.junit.Test; import static org.fest.assertions.api.Assertions.assertThat; public class ErrorFormatterTest { private static final int TYPE_CODE = 202; private static final int EXTRA_CODE = -218; @Test public void givenTypeAndExtra_whenFormattingMessage_thenReturnsExpectedMessageFormat() { String expectedFormat = "Type: 202, Extra: -218"; String actualFormat = ErrorFormatter.formatMessage(TYPE_CODE, EXTRA_CODE); assertThat(actualFormat).isEqualTo(expectedFormat); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/mediaplayer/LoadTimeoutTest.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import android.os.Handler; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.internal.Clock; import com.novoda.noplayer.model.LoadTimeout; import com.novoda.noplayer.model.Timeout; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import static org.mockito.Matchers.any; import static org.mockito.Mockito.verify; import static org.mockito.MockitoAnnotations.initMocks; public class LoadTimeoutTest { private static final Timeout ANY_TIME = Timeout.fromSeconds(0); @Mock private Clock clock; @Mock private Handler handler; @InjectMocks private LoadTimeout loadTimeout; @Before public void setUp() { initMocks(this); } @Test public void whenStartingATimeout_thenAnyPreviouslySetTimeoutRunnableAreRemoved() { loadTimeout.start(ANY_TIME, any(NoPlayer.LoadTimeoutCallback.class)); verify(handler).removeCallbacks(any(Runnable.class)); } @Test public void whenStartingATimeout_thenTheTimeoutRunnableIsPosted() { loadTimeout.start(ANY_TIME, any(NoPlayer.LoadTimeoutCallback.class)); verify(handler).post(any(Runnable.class)); } @Test public void whenCancelingATimeout_thenTimeoutRunnableIsRemoved() { loadTimeout.cancel(); verify(handler).removeCallbacks(any(Runnable.class)); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/mediaplayer/MediaPlayerInformationTest.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import android.os.Build; import com.novoda.noplayer.PlayerType; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; public class MediaPlayerInformationTest { @Rule public MockitoRule rule = MockitoJUnit.rule(); @Mock private MediaPlayerTypeReader playerTypeReader; private MediaPlayerInformation playerInformation; @Before public void setUp() { playerInformation = new MediaPlayerInformation(playerTypeReader); } @Test public void givenInternalNuPlayer_whenReadingName_thenReturnsMediaPlayerNuPlayer() { given(playerTypeReader.getPlayerType()).willReturn(AndroidMediaPlayerType.NU); String name = playerInformation.getName(); assertThat(name).isEqualTo("MediaPlayer: NuPlayer"); } @Test public void whenReadingVersion_thenReturnsAndroidBuildVersion() { String version = playerInformation.getVersion(); assertThat(version).isEqualTo(Build.VERSION.RELEASE); } @Test public void whenPlayerType_thenReturnsMediaPlayer() { PlayerType playerType = playerInformation.getPlayerType(); assertThat(playerType).isEqualTo(PlayerType.MEDIA_PLAYER); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/mediaplayer/NoPlayerMediaPlayerCreatorTest.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import android.content.Context; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; public class NoPlayerMediaPlayerCreatorTest { @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock private NoPlayerMediaPlayerCreator.InternalCreator internalCreator; @Mock private AndroidMediaPlayerImpl player; @Mock private Context context; private NoPlayerMediaPlayerCreator creator; @Before public void setUp() { creator = new NoPlayerMediaPlayerCreator(internalCreator); given(internalCreator.create(context)).willReturn(player); } @Test public void whenCreatingMediaPlayer_thenInitialisesPlayer() { creator.createMediaPlayer(context); verify(player).initialise(); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/mediaplayer/OnPotentialBuggyDriverLayoutListenerTest.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import android.view.View; import com.novoda.noplayer.NoPlayer; import org.junit.Rule; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import static org.mockito.Mockito.*; public class OnPotentialBuggyDriverLayoutListenerTest { private static final int ANY_DIMENSION_VALUE = 0; @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock NoPlayer player; @InjectMocks OnPotentialBuggyDriverLayoutListener buggyDriverLayoutListener; @Test public void givenStatusIsNotCorrupted_whenALayoutChangeOccurs_thenDoNotForceAlignNativeMediaPlayerStatus() { when(player.isPlaying()).thenReturn(false); onLayoutChange(); verify(player, never()).play(); } @Test public void givenStatusMightBeNotCorrupted_whenALayoutChangeOccurs_thenForceAlignNativeMediaPlayerStatus() { when(player.isPlaying()).thenReturn(true); onLayoutChange(); verify(player, atLeastOnce()).play(); } private void onLayoutChange() { buggyDriverLayoutListener.onLayoutChange( mock(View.class), ANY_DIMENSION_VALUE, ANY_DIMENSION_VALUE, ANY_DIMENSION_VALUE, ANY_DIMENSION_VALUE, ANY_DIMENSION_VALUE, ANY_DIMENSION_VALUE, ANY_DIMENSION_VALUE, ANY_DIMENSION_VALUE); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/mediaplayer/PlaybackStateCheckerTest.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import android.media.MediaPlayer; import java.util.Arrays; import java.util.Collection; import org.junit.Rule; import org.junit.Test; import org.junit.experimental.runners.Enclosed; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @RunWith(Enclosed.class) public class PlaybackStateCheckerTest { private static final MediaPlayer ANY_MEDIA_PLAYER = mock(MediaPlayer.class); private static final MediaPlayer NO_MEDIA_PLAYER = null; private static final boolean IS_IN_PLAYBACK_STATE = true; private static final boolean IS_NOT_IN_PLAYBACK_STATE = false; @RunWith(Parameterized.class) public static class CheckingPlaybackState { @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); private final MediaPlayer mediaPlayer; private final PlaybackStateChecker.PlaybackState playbackState; private final boolean expectedIsInPlaybackState; @Parameterized.Parameters(name = "MediaPlayer: {0} Playback state: {1}, isInPlaybackState: {2}") public static Collection parameters() { return Arrays.asList( new Object[]{NO_MEDIA_PLAYER, PlaybackStateChecker.PlaybackState.COMPLETED, IS_NOT_IN_PLAYBACK_STATE}, new Object[]{ANY_MEDIA_PLAYER, PlaybackStateChecker.PlaybackState.ERROR, IS_NOT_IN_PLAYBACK_STATE}, new Object[]{ANY_MEDIA_PLAYER, PlaybackStateChecker.PlaybackState.IDLE, IS_NOT_IN_PLAYBACK_STATE}, new Object[]{ANY_MEDIA_PLAYER, PlaybackStateChecker.PlaybackState.PREPARING, IS_NOT_IN_PLAYBACK_STATE}, new Object[]{ANY_MEDIA_PLAYER, PlaybackStateChecker.PlaybackState.PREPARED, IS_IN_PLAYBACK_STATE}, new Object[]{ANY_MEDIA_PLAYER, PlaybackStateChecker.PlaybackState.PLAYING, IS_IN_PLAYBACK_STATE}, new Object[]{ANY_MEDIA_PLAYER, PlaybackStateChecker.PlaybackState.PAUSED, IS_IN_PLAYBACK_STATE}, new Object[]{ANY_MEDIA_PLAYER, PlaybackStateChecker.PlaybackState.COMPLETED, IS_IN_PLAYBACK_STATE} ); } public CheckingPlaybackState(MediaPlayer mediaPlayer, PlaybackStateChecker.PlaybackState playbackState, boolean expectedIsInPlaybackState) { this.mediaPlayer = mediaPlayer; this.playbackState = playbackState; this.expectedIsInPlaybackState = expectedIsInPlaybackState; } @Test public void whenCheckingIsInPlaybackState_thenReturnsExpectedState() { PlaybackStateChecker playbackStateChecker = new PlaybackStateChecker(); boolean inPlaybackState = playbackStateChecker.isInPlaybackState(mediaPlayer, playbackState); assertThat(inPlaybackState).isEqualTo(expectedIsInPlaybackState); } } @RunWith(Parameterized.class) public static class CheckingIsPlaying { private static final boolean MEDIA_PLAYER_IS_PLAYING = true; private static final boolean MEDIA_PLAYER_IS_NOT_PLAYING = false; private static final boolean IS_PLAYING = true; private static final boolean IS_NOT_PLAYING = false; @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); private final MediaPlayer mediaPlayer; private final PlaybackStateChecker.PlaybackState playbackState; private final boolean expectedIsPlaying; @Parameterized.Parameters(name = "MediaPlayer: {0} mediaPlayer.isPlaying(): {1} Playback state: {2}, isPlaying: {3}") public static Collection parameters() { return Arrays.asList( new Object[]{NO_MEDIA_PLAYER, MEDIA_PLAYER_IS_PLAYING, PlaybackStateChecker.PlaybackState.COMPLETED, IS_NOT_PLAYING}, new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_PLAYING, PlaybackStateChecker.PlaybackState.ERROR, IS_NOT_PLAYING}, new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_PLAYING, PlaybackStateChecker.PlaybackState.IDLE, IS_NOT_PLAYING}, new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_PLAYING, PlaybackStateChecker.PlaybackState.PREPARING, IS_NOT_PLAYING}, new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_PLAYING, PlaybackStateChecker.PlaybackState.PREPARED, IS_PLAYING}, new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_PLAYING, PlaybackStateChecker.PlaybackState.PLAYING, IS_PLAYING}, new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_PLAYING, PlaybackStateChecker.PlaybackState.PAUSED, IS_PLAYING}, new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_PLAYING, PlaybackStateChecker.PlaybackState.COMPLETED, IS_PLAYING}, new Object[]{NO_MEDIA_PLAYER, MEDIA_PLAYER_IS_NOT_PLAYING, PlaybackStateChecker.PlaybackState.COMPLETED, IS_NOT_PLAYING}, new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_NOT_PLAYING, PlaybackStateChecker.PlaybackState.ERROR, IS_NOT_PLAYING}, new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_NOT_PLAYING, PlaybackStateChecker.PlaybackState.IDLE, IS_NOT_PLAYING}, new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_NOT_PLAYING, PlaybackStateChecker.PlaybackState.PREPARING, IS_NOT_PLAYING}, new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_NOT_PLAYING, PlaybackStateChecker.PlaybackState.PREPARED, IS_PLAYING}, new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_NOT_PLAYING, PlaybackStateChecker.PlaybackState.PLAYING, IS_PLAYING}, new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_NOT_PLAYING, PlaybackStateChecker.PlaybackState.PAUSED, IS_PLAYING}, new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_NOT_PLAYING, PlaybackStateChecker.PlaybackState.COMPLETED, IS_PLAYING} ); } public CheckingIsPlaying(MediaPlayer mediaPlayer, boolean mediaPlayerIsPlaying, PlaybackStateChecker.PlaybackState playbackState, boolean expectedIsPlaying) { this.mediaPlayer = mediaPlayer; this.playbackState = playbackState; this.expectedIsPlaying = expectedIsPlaying; if (mediaPlayer != null) { given(mediaPlayer.isPlaying()).willReturn(mediaPlayerIsPlaying); } } @Test public void whenCheckingIsPlaying_thenReturnsExpectedState() { PlaybackStateChecker playbackStateChecker = new PlaybackStateChecker(); boolean inPlaybackState = playbackStateChecker.isInPlaybackState(mediaPlayer, playbackState); assertThat(inPlaybackState).isEqualTo(expectedIsPlaying); } } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/internal/mediaplayer/PlayerCheckerTest.java ================================================ package com.novoda.noplayer.internal.mediaplayer; import android.os.Build; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; public class PlayerCheckerTest { private static final String PROP_USE_NU_PLAYER = "media.stagefright.use-nuplayer"; private static final String PROP_USE_AWESOME_PLAYER_PERSIST = "persist.sys.media.use-awesome"; private static final String PROP_USE_AWESOME_PLAYER_MEDIA = "media.stagefright.use-awesome"; @Mock SystemProperties systemProperties; private MediaPlayerTypeReader checker; @Before public void setUp() { initMocks(this); } @Test public void givenTheUserIsOnLollipopWhenTheAwesomePlayerPersistPropertyIsPresentThenAwesomePlayerIsDetected() throws Exception { givenTheUserIsOnOSVersion(Build.VERSION_CODES.LOLLIPOP); whenPropertyisPresent(PROP_USE_AWESOME_PLAYER_PERSIST); thenPlayerTypeIs(AndroidMediaPlayerType.AWESOME); } @Test public void givenTheUserIsOnLollipopWhenTheAwesomePlayerMediaPropertyIsPresentThenAwesomePlayerIsDetected() throws Exception { givenTheUserIsOnOSVersion(Build.VERSION_CODES.LOLLIPOP); whenPropertyisPresent(PROP_USE_AWESOME_PLAYER_MEDIA); thenPlayerTypeIs(AndroidMediaPlayerType.AWESOME); } @Test public void givenTheUserIsOnLollipopWhenNoAwesomePlayerMediaPropertyIsPresentThenNuPlayerIsDetected() { givenTheUserIsOnOSVersion(Build.VERSION_CODES.LOLLIPOP); whenNoPlayerPropertiesArePresent(); thenPlayerTypeIs(AndroidMediaPlayerType.NU); } @Test public void givenTheUserIsOnKitkatWhenTheNuPlayerPropertyIsPresentThenNuPlayerIsDetected() throws Exception { givenTheUserIsOnOSVersion(Build.VERSION_CODES.KITKAT); whenPropertyisPresent(PROP_USE_NU_PLAYER); thenPlayerTypeIs(AndroidMediaPlayerType.NU); } @Test public void givenTheUserIsOnKitkatWhenNoNuPlayerMediaPropertyIsPresentThenAwesomePlayerIsDetected() { givenTheUserIsOnOSVersion(Build.VERSION_CODES.KITKAT); whenNoPlayerPropertiesArePresent(); thenPlayerTypeIs(AndroidMediaPlayerType.AWESOME); } @Test public void givenTheUserIsNotAbleToReadSystemPropertiesWhenFetchingThePlayerTypeThenUnknownPlayerIsDetected() throws Exception { givenTheUserIsOnOSVersion(Build.VERSION_CODES.KITKAT); when(systemProperties.get(anyString())).thenThrow(new SystemProperties.MissingSystemPropertiesException(new Exception())); thenPlayerTypeIs(AndroidMediaPlayerType.UNKNOWN); } private void givenTheUserIsOnOSVersion(int deviceOSVersion) { checker = new MediaPlayerTypeReader(systemProperties, deviceOSVersion); } private void whenPropertyisPresent(String property) throws SystemProperties.MissingSystemPropertiesException { when(systemProperties.get(property)).thenReturn(Boolean.TRUE.toString()); } private void whenNoPlayerPropertiesArePresent() { // no-op because there is no work to do } private void thenPlayerTypeIs(AndroidMediaPlayerType playerType) { assertThat(checker.getPlayerType()).isEqualTo(playerType); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/model/AudioTracksTest.java ================================================ package com.novoda.noplayer.model; import com.novoda.noplayer.internal.exoplayer.mediasource.AudioTrackType; import java.util.Arrays; import java.util.Collections; import org.junit.Test; import static org.fest.assertions.api.Assertions.assertThat; public class AudioTracksTest { private static final PlayerAudioTrack MAIN_TRACK = PlayerAudioTrackFixture.aPlayerAudioTrack().withAudioTrackType(AudioTrackType.MAIN).build(); private static final PlayerAudioTrack ALTERNATIVE_TRACK = PlayerAudioTrackFixture.aPlayerAudioTrack().withAudioTrackType(AudioTrackType.ALTERNATIVE).build(); private static final int FIRST_INDEX = 0; private static final int EXPECTED_SIZE = 2; @Test public void givenAudioTracks_whenGettingTrack_thenReturnsTrack() { AudioTracks audioTracks = AudioTracks.from(Collections.singletonList(MAIN_TRACK)); PlayerAudioTrack playerAudioTrack = audioTracks.get(FIRST_INDEX); assertThat(playerAudioTrack).isEqualTo(MAIN_TRACK); } @Test public void givenAudioTracks_whenGettingSize_thenReturnsSize() { AudioTracks audioTracks = AudioTracks.from(Arrays.asList(MAIN_TRACK, ALTERNATIVE_TRACK)); int size = audioTracks.size(); assertThat(size).isEqualTo(EXPECTED_SIZE); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/model/EitherTest.java ================================================ package com.novoda.noplayer.model; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) public class EitherTest { @Mock private Either.Consumer leftConsumer; @Mock private Either.Consumer rightConsumer; @Test public void givenEitherContainsLeft_whenApplyingConsumers_thenRunsLeftConsumerWithCorrectValue() { String value = "foo"; Either either = Either.left(value); either.apply(leftConsumer, rightConsumer); verify(leftConsumer).accept(value); } @Test public void givenEitherContainsRight_whenApplyingConsumers_thenRunsRightConsumerWithCorrectValue() { Integer value = 42; Either either = Either.right(value); either.apply(leftConsumer, rightConsumer); verify(rightConsumer).accept(value); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/model/PlayerAudioTrackFixture.java ================================================ package com.novoda.noplayer.model; import com.novoda.noplayer.internal.exoplayer.mediasource.AudioTrackType; public class PlayerAudioTrackFixture { private int groupIndex = 0; private int formatIndex = 0; private String trackId = "id"; private String language = "english"; private String mimeType = ".mp4"; private int numberOfChannels = 1; private int frequency = 60; private AudioTrackType audioTrackType = AudioTrackType.MAIN; public static PlayerAudioTrackFixture aPlayerAudioTrack() { return new PlayerAudioTrackFixture(); } public PlayerAudioTrackFixture withGroupIndex(int groupIndex) { this.groupIndex = groupIndex; return this; } public PlayerAudioTrackFixture withFormatIndex(int formatIndex) { this.formatIndex = formatIndex; return this; } public PlayerAudioTrackFixture withTrackId(String trackId) { this.trackId = trackId; return this; } public PlayerAudioTrackFixture withLanguage(String language) { this.language = language; return this; } public PlayerAudioTrackFixture withMimeType(String mimeType) { this.mimeType = mimeType; return this; } public PlayerAudioTrackFixture withNumberOfChannels(int numberOfChannels) { this.numberOfChannels = numberOfChannels; return this; } public PlayerAudioTrackFixture withFrequency(int frequency) { this.frequency = frequency; return this; } public PlayerAudioTrackFixture withAudioTrackType(AudioTrackType audioTrackType) { this.audioTrackType = audioTrackType; return this; } public PlayerAudioTrack build() { return new PlayerAudioTrack( groupIndex, formatIndex, trackId, language, mimeType, numberOfChannels, frequency, audioTrackType ); } } ================================================ FILE: core/src/test/java/com/novoda/noplayer/model/PlayerVideoTrackFixture.java ================================================ package com.novoda.noplayer.model; import com.novoda.noplayer.ContentType; public class PlayerVideoTrackFixture { private int groupIndex = 0; private int formatIndex = 0; private String id = "id"; private ContentType contentType = ContentType.DASH; private int width = 1920; private int height = 1080; private int fps = 30; private int bitrate = 180000; public static PlayerVideoTrackFixture aPlayerVideoTrack() { return new PlayerVideoTrackFixture(); } private PlayerVideoTrackFixture() { // Uses static factory method. } public PlayerVideoTrackFixture withGroupIndex(int groupIndex) { this.groupIndex = groupIndex; return this; } public PlayerVideoTrackFixture withFormatIndex(int formatIndex) { this.formatIndex = formatIndex; return this; } public PlayerVideoTrackFixture withId(String id) { this.id = id; return this; } public PlayerVideoTrackFixture withContentType(ContentType contentType) { this.contentType = contentType; return this; } public PlayerVideoTrackFixture withWidth(int width) { this.width = width; return this; } public PlayerVideoTrackFixture withHeight(int height) { this.height = height; return this; } public PlayerVideoTrackFixture withFps(int fps) { this.fps = fps; return this; } public PlayerVideoTrackFixture withBitrate(int bitrate) { this.bitrate = bitrate; return this; } public PlayerVideoTrack build() { return new PlayerVideoTrack(groupIndex, formatIndex, id, contentType, width, height, fps, bitrate); } } ================================================ FILE: core/src/test/java/utils/ExceptionMatcher.java ================================================ package utils; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; public class ExceptionMatcher extends BaseMatcher { private final String expectedMessage; private final Class expectedExceptionClass; public static ExceptionMatcher matches(String message, Class expectedExceptionClass) { return new ExceptionMatcher(message, expectedExceptionClass); } private ExceptionMatcher(String expectedMessage, Class expectedExceptionClass) { this.expectedMessage = expectedMessage; this.expectedExceptionClass = expectedExceptionClass; } @Override public boolean matches(Object o) { Exception exception = (Exception) o; return expectedMessage.equals(exception.getMessage()) && exception.getClass().isAssignableFrom(expectedExceptionClass); } @Override public void describeTo(Description description) { description.appendText(String.format("<%s: %s>", expectedExceptionClass.getName(), expectedMessage)); } } ================================================ FILE: core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker ================================================ mock-maker-inline ================================================ FILE: demo/build.gradle ================================================ apply plugin: 'com.android.application' android { compileSdkVersion 28 buildToolsVersion '28.0.3' defaultConfig { applicationId 'com.novoda.demo' minSdkVersion 16 targetSdkVersion 28 versionCode 1 versionName '1.0' } lintOptions { lintConfig teamPropsFile('static-analysis/lint-config.xml') abortOnError true warningsAsErrors true } compileOptions { targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation project(':core') implementation 'com.android.support:appcompat-v7:28.0.0' testImplementation 'junit:junit:4.12' testImplementation 'com.google.truth:truth:0.44' } ================================================ FILE: demo/src/main/AndroidManifest.xml ================================================ ================================================ FILE: demo/src/main/java/com/novoda/demo/AndroidControllerView.java ================================================ package com.novoda.demo; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.TextView; public class AndroidControllerView extends LinearLayout implements ControllerView { private ImageView playPauseButton; private TextView elapsedTimeView; private SeekBar progressView; private TextView timeRemainingView; private ImageView volumeOnOffView; public AndroidControllerView(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs, 0); } public AndroidControllerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); super.setOrientation(HORIZONTAL); } @Override public final void setOrientation(int orientation) { throw new IllegalAccessError("This layout only supports horizontal orientation"); } @Override protected void onFinishInflate() { super.onFinishInflate(); LayoutInflater.from(getContext()).inflate(R.layout.merge_player_controls, this, true); playPauseButton = (ImageView) findViewById(R.id.player_controls_play_pause); elapsedTimeView = (TextView) findViewById(R.id.player_controls_elapsed_time); progressView = (SeekBar) findViewById(R.id.player_controls_progress); timeRemainingView = (TextView) findViewById(R.id.player_controls_time_remaining); volumeOnOffView = findViewById(R.id.player_controls_volume_on_off); } @Override public void setPaused() { playPauseButton.setImageResource(R.drawable.play); } @Override public void setPlaying() { playPauseButton.setImageResource(R.drawable.pause); } @Override public void updateContentProgress(int progress) { progressView.setProgress(progress); } @Override public void updateBufferProgress(int buffer) { progressView.setSecondaryProgress(buffer); } @Override public void updateElapsedTime(String elapsedTime) { elapsedTimeView.setText(elapsedTime); } @Override public void updateTimeRemaining(String timeRemaining) { timeRemainingView.setText(timeRemaining); } @Override public void setTogglePlayPauseAction(final TogglePlayPauseAction togglePlayPauseAction) { playPauseButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { togglePlayPauseAction.perform(); } }); } @Override public void setSeekAction(final SeekAction seekAction) { progressView.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { // Not required. } @Override public void onStartTrackingTouch(SeekBar seekBar) { // Not required. } @Override public void onStopTrackingTouch(SeekBar seekBar) { int progress = seekBar.getProgress(); int max = seekBar.getMax(); seekAction.perform(progress, max); } }); } @Override public void setVolumeOn() { volumeOnOffView.setImageResource(R.drawable.volume_on); } @Override public void setVolumeOff() { volumeOnOffView.setImageResource(R.drawable.volume_off); } @Override public void setToggleVolumeOnOffAction(final ToggleVolumeOnOffAction toggleVolumeOnOffAction) { volumeOnOffView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { toggleVolumeOnOffAction.perform(); } }); } } ================================================ FILE: demo/src/main/java/com/novoda/demo/ControllerView.java ================================================ package com.novoda.demo; interface ControllerView { void setPaused(); void setPlaying(); void updateContentProgress(int progress); void updateBufferProgress(int buffer); void updateElapsedTime(String elapsedTime); void updateTimeRemaining(String timeRemaining); void setTogglePlayPauseAction(TogglePlayPauseAction togglePlayPauseAction); void setSeekAction(SeekAction seekAction); void setVolumeOn(); void setVolumeOff(); void setToggleVolumeOnOffAction(ToggleVolumeOnOffAction toggleVolumeOnOffAction); interface TogglePlayPauseAction { void perform(); } interface SeekAction { void perform(int progress, int max); } interface ToggleVolumeOnOffAction { void perform(); } } ================================================ FILE: demo/src/main/java/com/novoda/demo/DataPostingModularDrm.java ================================================ package com.novoda.demo; import com.novoda.noplayer.drm.ModularDrmKeyRequest; import com.novoda.noplayer.drm.StreamingModularDrm; class DataPostingModularDrm implements StreamingModularDrm { private final String url; DataPostingModularDrm(String url) { this.url = url; } @Override public byte[] executeKeyRequest(ModularDrmKeyRequest request) throws DrmRequestException { return HttpClient.post(url, request.data()); } } ================================================ FILE: demo/src/main/java/com/novoda/demo/DemoPresenter.java ================================================ package com.novoda.demo; import android.net.Uri; import com.novoda.noplayer.Listeners; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.Options; import com.novoda.noplayer.PlayerState; import com.novoda.noplayer.PlayerView; class DemoPresenter { private final ControllerView controllerView; private final NoPlayer noPlayer; private final Listeners listeners; private final PlayerView playerView; DemoPresenter(ControllerView controllerView, NoPlayer noPlayer, Listeners listeners, PlayerView playerView) { this.controllerView = controllerView; this.noPlayer = noPlayer; this.listeners = listeners; this.playerView = playerView; } void startPresenting(Uri uri, Options options) { listeners.addPreparedListener(playOnPrepared); listeners.addStateChangedListener(updatePlayPause); listeners.addHeartbeatCallback(updateProgress); controllerView.setTogglePlayPauseAction(onTogglePlayPause); controllerView.setSeekAction(onSeekPerformed); controllerView.setToggleVolumeOnOffAction(onToggleVolume); noPlayer.attach(playerView); noPlayer.loadVideo(uri, options); } private final NoPlayer.PreparedListener playOnPrepared = new NoPlayer.PreparedListener() { @Override public void onPrepared(PlayerState playerState) { noPlayer.play(); } }; private final NoPlayer.StateChangedListener updatePlayPause = new NoPlayer.StateChangedListener() { @Override public void onVideoPlaying() { controllerView.setPlaying(); } @Override public void onVideoPaused() { controllerView.setPaused(); } @Override public void onVideoStopped() { // Not required. } }; private final NoPlayer.HeartbeatCallback updateProgress = new NoPlayer.HeartbeatCallback() { @Override public void onBeat(NoPlayer player) { long positionInMillis = player.playheadPositionInMillis(); long durationInMillis = player.mediaDurationInMillis(); int bufferPercentage = player.bufferPercentage(); updateProgress(positionInMillis, durationInMillis, bufferPercentage); updateTiming(positionInMillis, durationInMillis); } }; private void updateProgress(long positionInMillis, long durationInMillis, int bufferPercentage) { int progressAsIncrements = ProgressCalculator.progressAsIncrements(positionInMillis, durationInMillis); int bufferAsIncrements = ProgressCalculator.bufferAsIncrements(bufferPercentage); controllerView.updateContentProgress(progressAsIncrements); controllerView.updateBufferProgress(bufferAsIncrements); } private void updateTiming(long positionInMillis, long durationInMillis) { long remainingDuration = durationInMillis - positionInMillis; controllerView.updateElapsedTime(TimeFormatter.asHoursMinutesSeconds(positionInMillis)); controllerView.updateTimeRemaining(TimeFormatter.asHoursMinutesSeconds(remainingDuration)); } private final ControllerView.TogglePlayPauseAction onTogglePlayPause = new ControllerView.TogglePlayPauseAction() { @Override public void perform() { if (noPlayer.isPlaying()) { noPlayer.pause(); } else { noPlayer.play(); } } }; private final ControllerView.SeekAction onSeekPerformed = new ControllerView.SeekAction() { @Override public void perform(int progress, int max) { long seekToPosition = ProgressCalculator.seekToPosition(noPlayer.mediaDurationInMillis(), progress, max); noPlayer.seekTo(seekToPosition); } }; private final ControllerView.ToggleVolumeOnOffAction onToggleVolume = new ControllerView.ToggleVolumeOnOffAction() { @Override public void perform() { boolean isSoundOn = Float.compare(noPlayer.getVolume(), 0f) > 0; if (isSoundOn) { noPlayer.setVolume(0f); controllerView.setVolumeOff(); } else { noPlayer.setVolume(1f); controllerView.setVolumeOn(); } } }; void stopPresenting() { noPlayer.stop(); noPlayer.release(); } } ================================================ FILE: demo/src/main/java/com/novoda/demo/DialogCreator.java ================================================ package com.novoda.demo; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.widget.ArrayAdapter; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.model.AudioTracks; import com.novoda.noplayer.model.PlayerAudioTrack; import com.novoda.noplayer.model.PlayerSubtitleTrack; import com.novoda.noplayer.model.PlayerVideoTrack; import java.util.ArrayList; import java.util.List; import java.util.Locale; class DialogCreator { private static final String VIDEO_TRACK_MESSAGE_FORMAT = "ID: %s Quality: %s"; private static final String AUDIO_TRACK_MESSAGE_FORMAT = "ID: %s Type: %s"; private static final int AUTO_TRACK_POSITION = 0; private final Context context; private final NoPlayer noPlayer; DialogCreator(Context context, NoPlayer noPlayer) { this.context = context; this.noPlayer = noPlayer; } void showVideoSelectionDialog() { final List videoTracks = noPlayer.getVideoTracks(); ArrayAdapter adapter = new ArrayAdapter<>(context, R.layout.list_item); adapter.addAll(mapVideoTrackToLabel(videoTracks)); new AlertDialog.Builder(context) .setTitle("Select Video track") .setAdapter(adapter, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int position) { if (position == AUTO_TRACK_POSITION) { noPlayer.clearVideoTrackSelection(); } else { PlayerVideoTrack videoTrack = videoTracks.get(position - 1); noPlayer.selectVideoTrack(videoTrack); } } }) .create() .show(); } private List mapVideoTrackToLabel(List videoTracks) { List labels = new ArrayList<>(); labels.add("Auto"); for (PlayerVideoTrack videoTrack : videoTracks) { String message = String.format(VIDEO_TRACK_MESSAGE_FORMAT, videoTrack.id(), videoTrack.height()); labels.add(message); } return labels; } void showAudioSelectionDialog() { final AudioTracks audioTracks = noPlayer.getAudioTracks(); ArrayAdapter adapter = new ArrayAdapter<>(context, R.layout.list_item); adapter.addAll(mapAudioTrackToLabel(audioTracks)); new AlertDialog.Builder(context) .setTitle("Select audio track") .setAdapter(adapter, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int position) { if (position == AUTO_TRACK_POSITION) { noPlayer.clearAudioTrackSelection(); } else { PlayerAudioTrack audioTrack = audioTracks.get(position - 1); noPlayer.selectAudioTrack(audioTrack); } } }) .create() .show(); } private List mapAudioTrackToLabel(AudioTracks audioTracks) { List labels = new ArrayList<>(); labels.add("Auto"); for (PlayerAudioTrack audioTrack : audioTracks) { String label = String.format( Locale.UK, AUDIO_TRACK_MESSAGE_FORMAT, audioTrack.trackId(), audioTrack.audioTrackType() ); labels.add(label); } return labels; } void showSubtitleSelectionDialog() { final List subtitleTracks = noPlayer.getSubtitleTracks(); ArrayAdapter adapter = new ArrayAdapter<>(context, R.layout.list_item); adapter.addAll(mapSubtitleTrackToLabel(subtitleTracks)); new AlertDialog.Builder(context) .setTitle("Select subtitle track") .setAdapter(adapter, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int position) { if (position == AUTO_TRACK_POSITION) { noPlayer.hideSubtitleTrack(); } else { PlayerSubtitleTrack subtitleTrack = subtitleTracks.get(position - 1); noPlayer.showSubtitleTrack(subtitleTrack); } } }) .create() .show(); } private List mapSubtitleTrackToLabel(List subtitleTracks) { List labels = new ArrayList<>(); labels.add("Dismiss subtitles"); for (PlayerSubtitleTrack subtitleTrack : subtitleTracks) { labels.add("Group: " + subtitleTrack.groupIndex() + " Format: " + subtitleTrack.formatIndex()); } return labels; } } ================================================ FILE: demo/src/main/java/com/novoda/demo/HttpClient.java ================================================ package com.novoda.demo; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; final class HttpClient { private static final String POST = "POST"; private static final int RESPONSE_BUFFER_SIZE = 16384; private HttpClient() { // Not instantiable } static byte[] post(String url, byte[] data) { HttpURLConnection connection = null; try { connection = (HttpURLConnection) new URL(url).openConnection(); connection.setRequestMethod(POST); connection.setDoOutput(true); connection.setDoInput(true); DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream()); outputStream.write(data); outputStream.flush(); outputStream.close(); return readResponseFrom(connection); } catch (IOException e) { throw new HttpClientException(e); } finally { release(connection); } } private static void release(HttpURLConnection connection) { if (connection != null) { connection.disconnect(); } } private static byte[] readResponseFrom(URLConnection connection) throws IOException { InputStream inputStream = null; ByteArrayOutputStream buffer = null; try { inputStream = connection.getInputStream(); buffer = new ByteArrayOutputStream(); int currentReadPosition; byte[] readHolder = new byte[RESPONSE_BUFFER_SIZE]; while ((currentReadPosition = inputStream.read(readHolder, 0, readHolder.length)) != -1) { buffer.write(readHolder, 0, currentReadPosition); } buffer.flush(); return buffer.toByteArray(); } catch (IOException e) { throw new HttpClientException(e); } finally { release(inputStream, buffer); } } private static void release(InputStream inputStream, ByteArrayOutputStream buffer) throws IOException { if (inputStream != null) { inputStream.close(); } if (buffer != null) { buffer.close(); } } private static class HttpClientException extends RuntimeException { HttpClientException(Throwable cause) { super(cause); } } } ================================================ FILE: demo/src/main/java/com/novoda/demo/MainActivity.java ================================================ package com.novoda.demo; import android.app.Activity; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.Toast; import com.novoda.noplayer.ContentType; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.Options; import com.novoda.noplayer.OptionsBuilder; import com.novoda.noplayer.PlayerBuilder; import com.novoda.noplayer.PlayerView; import com.novoda.noplayer.internal.utils.NoPlayerLog; public class MainActivity extends Activity { private static final String URI_VIDEO_WIDEVINE_EXAMPLE_MODULAR_MPD = "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd"; private static final String EXAMPLE_MODULAR_LICENSE_SERVER_PROXY = "https://proxy.uat.widevine.com/proxy?provider=widevine_test"; private static final int HALF_A_SECOND_IN_MILLIS = 500; private static final int TWO_MEGABITS = 2000000; private static final int MAX_VIDEO_BITRATE = 800000; private NoPlayer player; private DemoPresenter demoPresenter; private DialogCreator dialogCreator; private CheckBox hdSelectionCheckBox; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); NoPlayerLog.setLoggingEnabled(true); setContentView(R.layout.activity_main); PlayerView playerView = findViewById(R.id.player_view); View videoSelectionButton = findViewById(R.id.button_video_selection); View audioSelectionButton = findViewById(R.id.button_audio_selection); View subtitleSelectionButton = findViewById(R.id.button_subtitle_selection); hdSelectionCheckBox = findViewById(R.id.button_hd_selection); ControllerView controllerView = findViewById(R.id.controller_view); videoSelectionButton.setOnClickListener(showVideoSelectionDialog); audioSelectionButton.setOnClickListener(showAudioSelectionDialog); subtitleSelectionButton.setOnClickListener(showSubtitleSelectionDialog); hdSelectionCheckBox.setOnCheckedChangeListener(toggleHdSelection); DataPostingModularDrm drmHandler = new DataPostingModularDrm(EXAMPLE_MODULAR_LICENSE_SERVER_PROXY); player = new PlayerBuilder() .withWidevineModularStreamingDrm(drmHandler) .withDowngradedSecureDecoder() .withUserAgent("Android/Linux") .allowCrossProtocolRedirects() .build(this); demoPresenter = new DemoPresenter(controllerView, player, player.getListeners(), playerView); dialogCreator = new DialogCreator(this, player); player.getListeners().addDroppedVideoFrames(new NoPlayer.DroppedVideoFramesListener() { @Override public void onDroppedVideoFrames(int droppedFrames, long elapsedMsSinceLastDroppedFrames) { Log.v(getClass().toString(), "dropped frames: " + droppedFrames + " since: " + elapsedMsSinceLastDroppedFrames + "ms"); } }); } @Override protected void onStart() { super.onStart(); Uri uri = Uri.parse(URI_VIDEO_WIDEVINE_EXAMPLE_MODULAR_MPD); Options options = new OptionsBuilder() .withContentType(ContentType.DASH) .withMinDurationBeforeQualityIncreaseInMillis(HALF_A_SECOND_IN_MILLIS) .withMaxInitialBitrate(TWO_MEGABITS) .withMaxVideoBitrate(getMaxVideoBitrate()) .build(); demoPresenter.startPresenting(uri, options); } private int getMaxVideoBitrate() { if (hdSelectionCheckBox.isChecked()) { return Integer.MAX_VALUE; } return MAX_VIDEO_BITRATE; } private final View.OnClickListener showVideoSelectionDialog = new View.OnClickListener() { @Override public void onClick(View v) { if (player.getVideoTracks().isEmpty()) { Toast.makeText(MainActivity.this, "no additional video tracks available!", Toast.LENGTH_LONG).show(); } else { dialogCreator.showVideoSelectionDialog(); } } }; private final View.OnClickListener showAudioSelectionDialog = new View.OnClickListener() { @Override public void onClick(View v) { dialogCreator.showAudioSelectionDialog(); } }; private final View.OnClickListener showSubtitleSelectionDialog = new View.OnClickListener() { @Override public void onClick(View v) { if (player.getSubtitleTracks().isEmpty()) { Toast.makeText(MainActivity.this, "no subtitles available!", Toast.LENGTH_LONG).show(); } else { dialogCreator.showSubtitleSelectionDialog(); } } }; private final CompoundButton.OnCheckedChangeListener toggleHdSelection = new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (isChecked) { player.clearMaxVideoBitrate(); } else { player.setMaxVideoBitrate(MAX_VIDEO_BITRATE); } } }; @Override protected void onStop() { demoPresenter.stopPresenting(); super.onStop(); } } ================================================ FILE: demo/src/main/java/com/novoda/demo/ProgressCalculator.java ================================================ package com.novoda.demo; final class ProgressCalculator { private static final int MAX_PROGRESS_INCREMENTS = 100; private static final int MAX_PROGRESS_PERCENT = 100; private ProgressCalculator() { // Uses static methods. } static int progressAsIncrements(long positionInMillis, long durationInMillis) { double percentageOfDuration = positionInMillis / (float) durationInMillis; return (int) (MAX_PROGRESS_INCREMENTS * percentageOfDuration); } static int bufferAsIncrements(int bufferPercentage) { return (bufferPercentage * MAX_PROGRESS_INCREMENTS) / MAX_PROGRESS_PERCENT; } static long seekToPosition(long durationInMillis, int progress, int max) { float progressMultiplier = (float) progress / max; return (long) (durationInMillis * progressMultiplier); } } ================================================ FILE: demo/src/main/java/com/novoda/demo/TimeFormatter.java ================================================ package com.novoda.demo; import java.util.Locale; import java.util.concurrent.TimeUnit; final class TimeFormatter { private TimeFormatter() { // Uses static methods. } static String asHoursMinutesSeconds(long timeInMillis) { long hours = TimeUnit.MILLISECONDS.toHours(timeInMillis); long minutes = TimeUnit.MILLISECONDS.toMinutes(timeInMillis - TimeUnit.HOURS.toMillis(hours)); long seconds = TimeUnit.MILLISECONDS.toSeconds(timeInMillis - TimeUnit.HOURS.toMillis(hours) - TimeUnit.MINUTES.toMillis(minutes)); if (hours > 0) { return String.format(Locale.getDefault(), "%d:%02d:%02d", hours, minutes, seconds); } else { return String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds); } } } ================================================ FILE: demo/src/main/res/drawable/progress.xml ================================================ ================================================ FILE: demo/src/main/res/drawable/thumb.xml ================================================ ================================================ FILE: demo/src/main/res/layout/activity_main.xml ================================================