diff --git a/README.md b/README.md index f74cda84ee..3de86d21a3 100644 --- a/README.md +++ b/README.md @@ -30,17 +30,39 @@ repositories { } ``` -Next, include the following in your module's `build.gradle` file: +Next add a gradle compile dependency to the `build.gradle` file of your app +module. The following will add a dependency to the full ExoPlayer library: ```gradle -compile 'com.google.android.exoplayer:exoplayer:rX.X.X' +compile 'com.google.android.exoplayer:exoplayer:r2.X.X' ``` -where `rX.X.X` is the your preferred version. For the latest version, see the -project's [Releases][]. For more details, see the project on [Bintray][]. +where `r2.X.X` is your preferred version. Alternatively, you can depend on only +the library modules that you actually need. For example the following will add +dependencies on the Core, DASH and UI library modules, as might be required for +an app that plays DASH content: -[Releases]: https://github.com/google/ExoPlayer/releases -[Bintray]: https://bintray.com/google/exoplayer/exoplayer/view +```gradle +compile 'com.google.android.exoplayer:exoplayer-core:r2.X.X' +compile 'com.google.android.exoplayer:exoplayer-dash:r2.X.X' +compile 'com.google.android.exoplayer:exoplayer-ui:r2.X.X' +``` + +The available modules are listed below. Adding a dependency to the full +ExoPlayer library is equivalent to adding dependencies on all of the modules +individually. + +* `exoplayer-core`: Core functionality (required). +* `exoplayer-dash`: Support for DASH content. +* `exoplayer-hls`: Support for HLS content. +* `exoplayer-smoothstreaming`: Support for SmoothStreaming content. +* `exoplayer-ui`: UI components and resources for use with ExoPlayer. + +For more details, see the project on [Bintray][]. For information about the +latest versions, see the [Release notes][]. + +[Bintray]: https://bintray.com/google/exoplayer +[Release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md ## Developing ExoPlayer ## diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a0c750660d..d216e767b0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,38 @@ # Release notes # +### r2.4.0 ### + +* New modular library structure. You can read more about depending on individual + library modules + [here](https://medium.com/google-exoplayer/exoplayers-new-modular-structure-a916c0874907). +* Variable speed playback support on API level 16+. You can read more about + changing the playback speed + [here](https://medium.com/google-exoplayer/variable-speed-playback-with-exoplayer-e6e6a71e0343) + ([#26](https://github.com/google/ExoPlayer/issues/26)). +* New time bar view, including support for displaying ad break markers. +* Support DVB subtitles in MPEG-TS and MKV. +* Support adaptive playback for audio only DASH, HLS and SmoothStreaming + ([#1975](https://github.com/google/ExoPlayer/issues/1975)). +* Support for setting extractor flags on DefaultExtractorsFactory + ([#2657](https://github.com/google/ExoPlayer/issues/2657)). +* Support injecting custom renderers into SimpleExoPlayer using a new + RenderersFactory interface. +* Correctly set ExoPlayer's internal thread priority to `THREAD_PRIORITY_AUDIO`. +* TX3G: Support styling and positioning. +* FLV: + * Support MP3 in FLV. + * Skip unhandled metadata rather than failing + ([#2634](https://github.com/google/ExoPlayer/issues/2634)). + * Fix potential OutOfMemory errors. +* ID3: Better handle malformed ID3 data + ([#2604](https://github.com/google/ExoPlayer/issues/2604), + [#2663](https://github.com/google/ExoPlayer/issues/2663)). +* FFmpeg extension: Fixed build instructions + ([#2561](https://github.com/google/ExoPlayer/issues/2561)). +* VP9 extension: Reduced binary size. +* FLAC extension: Enabled 64 bit targets. +* Misc bugfixes. + ### r2.3.1 ### * Fix NPE enabling WebVTT subtitles in DASH streams @@ -15,32 +48,31 @@ rendering. You can read more about the GVR extension [here](https://medium.com/google-exoplayer/spatial-audio-with-exoplayer-and-gvr-cecb00e9da5f#.xdjebjd7g). * DASH improvements: - * Support embedded CEA-608 closed captions - ([#2362](https://github.com/google/ExoPlayer/issues/2362)). - * Support embedded EMSG events - ([#2176](https://github.com/google/ExoPlayer/issues/2176)). - * Support mspr:pro manifest element - ([#2386](https://github.com/google/ExoPlayer/issues/2386)). - * Correct handling of empty segment indices at the start of live events - ([#1865](https://github.com/google/ExoPlayer/issues/1865)). + * Support embedded CEA-608 closed captions + ([#2362](https://github.com/google/ExoPlayer/issues/2362)). + * Support embedded EMSG events + ([#2176](https://github.com/google/ExoPlayer/issues/2176)). + * Support mspr:pro manifest element + ([#2386](https://github.com/google/ExoPlayer/issues/2386)). + * Correct handling of empty segment indices at the start of live events + ([#1865](https://github.com/google/ExoPlayer/issues/1865)). * HLS improvements: - * Respect initial track selection - ([#2353](https://github.com/google/ExoPlayer/issues/2353)). - * Reduced frequency of media playlist requests when playback position is - close to the live edge - ([#2548](https://github.com/google/ExoPlayer/issues/2548)). - * Exposed the master playlist through ExoPlayer.getCurrentManifest() - ([#2537](https://github.com/google/ExoPlayer/issues/2537)). - * Support CLOSED-CAPTIONS #EXT-X-MEDIA type - ([#341](https://github.com/google/ExoPlayer/issues/341)). - * Fixed handling of negative values in #EXT-X-SUPPORT - ([#2495](https://github.com/google/ExoPlayer/issues/2495)). - * Fixed potential endless buffering state for streams with WebVTT subtitles - ([#2424](https://github.com/google/ExoPlayer/issues/2424)). + * Respect initial track selection + ([#2353](https://github.com/google/ExoPlayer/issues/2353)). + * Reduced frequency of media playlist requests when playback position is close + to the live edge ([#2548](https://github.com/google/ExoPlayer/issues/2548)). + * Exposed the master playlist through ExoPlayer.getCurrentManifest() + ([#2537](https://github.com/google/ExoPlayer/issues/2537)). + * Support CLOSED-CAPTIONS #EXT-X-MEDIA type + ([#341](https://github.com/google/ExoPlayer/issues/341)). + * Fixed handling of negative values in #EXT-X-SUPPORT + ([#2495](https://github.com/google/ExoPlayer/issues/2495)). + * Fixed potential endless buffering state for streams with WebVTT subtitles + ([#2424](https://github.com/google/ExoPlayer/issues/2424)). * MPEG-TS improvements: - * Support for multiple programs. - * Support for multiple closed captions and caption service descriptors - ([#2161](https://github.com/google/ExoPlayer/issues/2161)). + * Support for multiple programs. + * Support for multiple closed captions and caption service descriptors + ([#2161](https://github.com/google/ExoPlayer/issues/2161)). * MP3: Add `FLAG_ENABLE_CONSTANT_BITRATE_SEEKING` extractor option to enable constant bitrate seeking in MP3 files that would otherwise be unseekable ([#2445](https://github.com/google/ExoPlayer/issues/2445)). @@ -135,15 +167,15 @@ * HLS: Support for seeking in live streams ([#87](https://github.com/google/ExoPlayer/issues/87)). * HLS: Improved support: - * Support for EXT-X-PROGRAM-DATE-TIME - ([#747](https://github.com/google/ExoPlayer/issues/747)). - * Improved handling of sample timestamps and their alignment across variants - and renditions. - * Fix issue that could cause playbacks to get stuck in an endless initial - buffering state. - * Correctly propagate BehindLiveWindowException instead of - IndexOutOfBoundsException exception - ([#1695](https://github.com/google/ExoPlayer/issues/1695)). + * Support for EXT-X-PROGRAM-DATE-TIME + ([#747](https://github.com/google/ExoPlayer/issues/747)). + * Improved handling of sample timestamps and their alignment across variants + and renditions. + * Fix issue that could cause playbacks to get stuck in an endless initial + buffering state. + * Correctly propagate BehindLiveWindowException instead of + IndexOutOfBoundsException exception + ([#1695](https://github.com/google/ExoPlayer/issues/1695)). * MP3/MP4: Support for ID3 metadata, including embedded album art ([#979](https://github.com/google/ExoPlayer/issues/979)). * Improved customization of UI components. You can read about customization of @@ -153,19 +185,19 @@ MediaPeriod transitions. * EIA608: Support for caption styling and positioning. * MPEG-TS: Improved support: - * Support injection of custom TS payload readers. - * Support injection of custom section payload readers. - * Support SCTE-35 splice information messages. - * Support multiple table sections in a single PSI section. - * Fix NullPointerException when an unsupported stream type is encountered - ([#2149](https://github.com/google/ExoPlayer/issues/2149)). - * Avoid failure when expected ID3 header not found - ([#1966](https://github.com/google/ExoPlayer/issues/1966)). + * Support injection of custom TS payload readers. + * Support injection of custom section payload readers. + * Support SCTE-35 splice information messages. + * Support multiple table sections in a single PSI section. + * Fix NullPointerException when an unsupported stream type is encountered + ([#2149](https://github.com/google/ExoPlayer/issues/2149)). + * Avoid failure when expected ID3 header not found + ([#1966](https://github.com/google/ExoPlayer/issues/1966)). * Improvements to the upstream cache package. - * Support caching of media segments for DASH, HLS and SmoothStreaming. Note - that caching of manifest and playlist files is still not supported in the - (normal) case where the corresponding responses are compressed. - * Support caching for ExtractorMediaSource based playbacks. + * Support caching of media segments for DASH, HLS and SmoothStreaming. Note + that caching of manifest and playlist files is still not supported in the + (normal) case where the corresponding responses are compressed. + * Support caching for ExtractorMediaSource based playbacks. * Improved flexibility of SimpleExoPlayer ([#2102](https://github.com/google/ExoPlayer/issues/2102)). * Fix issue where only the audio of a video would play due to capability @@ -237,63 +269,62 @@ some of the motivations behind ExoPlayer 2.x structure and class names have also been sanitized. Read more [here](https://medium.com/google-exoplayer/exoplayer-2-x-new-package-and-class-names-ef8e1d9ba96f#.lv8sd4nez). * Key architectural changes: - * Late binding between rendering and media source components. Allows the - same rendering components to be re-used from one playback to another. - Enables features such as gapless playback through playlists and DASH - multi-period support. - * Improved track selection design. More details can be found - [here](https://medium.com/google-exoplayer/exoplayer-2-x-track-selection-2b62ff712cc9#.n00zo76b6). - * LoadControl now used to control buffering and loading across all playback - types. - * Media source components given additional structure. A new MediaSource - class has been introduced. MediaSources expose Timelines that describe the - media they expose, and can consist of multiple MediaPeriods. This enables - features such as seeking in live playbacks and DASH multi-period support. - * Responsibility for loading the initial DASH/SmoothStreaming/HLS manifest - is promoted to the corresponding MediaSource components and is no longer - the application's responsibility. - * Higher level abstractions such as SimpleExoPlayer have been added to the - library. These make the library easier to use for common use cases. The - demo app is halved in size as a result, whilst at the same time gaining - more functionality. Read more - [here](https://medium.com/google-exoplayer/exoplayer-2-x-improved-demo-app-d97171aaaaa1). - * Enhanced library support for implementing audio extensions. Read more - [here](https://medium.com/google-exoplayer/exoplayer-2-x-new-audio-features-cfb26c2883a#.ua75vu4s3). - * Format and MediaFormat are replaced by a single Format class. + * Late binding between rendering and media source components. Allows the same + rendering components to be re-used from one playback to another. Enables + features such as gapless playback through playlists and DASH multi-period + support. + * Improved track selection design. More details can be found + [here](https://medium.com/google-exoplayer/exoplayer-2-x-track-selection-2b62ff712cc9#.n00zo76b6). + * LoadControl now used to control buffering and loading across all playback + types. + * Media source components given additional structure. A new MediaSource class + has been introduced. MediaSources expose Timelines that describe the media + they expose, and can consist of multiple MediaPeriods. This enables features + such as seeking in live playbacks and DASH multi-period support. + * Responsibility for loading the initial DASH/SmoothStreaming/HLS manifest is + promoted to the corresponding MediaSource components and is no longer the + application's responsibility. + * Higher level abstractions such as SimpleExoPlayer have been added to the + library. These make the library easier to use for common use cases. The demo + app is halved in size as a result, whilst at the same time gaining more + functionality. Read more + [here](https://medium.com/google-exoplayer/exoplayer-2-x-improved-demo-app-d97171aaaaa1). + * Enhanced library support for implementing audio extensions. Read more + [here](https://medium.com/google-exoplayer/exoplayer-2-x-new-audio-features-cfb26c2883a#.ua75vu4s3). + * Format and MediaFormat are replaced by a single Format class. * Key new features: - * Playlist support. Includes support for gapless playback between playlist - items and consistent application of LoadControl and TrackSelector policies - when transitioning between items - ([#1270](https://github.com/google/ExoPlayer/issues/1270)). - * Seeking in live playbacks for DASH and SmoothStreaming - ([#291](https://github.com/google/ExoPlayer/issues/291)). - * DASH multi-period support - ([#557](https://github.com/google/ExoPlayer/issues/557)). - * MediaSource composition allows MediaSources to be concatenated into a - playlist, merged and looped. Read more - [here](https://medium.com/google-exoplayer/exoplayer-2-x-mediasource-composition-6c285fcbca1f#.zfha8qupz). - * Looping support (see above) - ([#490](https://github.com/google/ExoPlayer/issues/490)). - * Ability to query information about all tracks in a piece of media - (including those not supported by the device) - ([#1121](https://github.com/google/ExoPlayer/issues/1121)). - * Improved player controls. - * Support for PSSH in fMP4 moof atoms - ([#1143](https://github.com/google/ExoPlayer/issues/1143)). - * Support for Opus in Ogg - ([#1447](https://github.com/google/ExoPlayer/issues/1447)). - * CacheDataSource support for standalone media file playbacks (mp3, mp4 - etc). - * FFMPEG extension (for audio only). + * Playlist support. Includes support for gapless playback between playlist + items and consistent application of LoadControl and TrackSelector policies + when transitioning between items + ([#1270](https://github.com/google/ExoPlayer/issues/1270)). + * Seeking in live playbacks for DASH and SmoothStreaming + ([#291](https://github.com/google/ExoPlayer/issues/291)). + * DASH multi-period support + ([#557](https://github.com/google/ExoPlayer/issues/557)). + * MediaSource composition allows MediaSources to be concatenated into a + playlist, merged and looped. Read more + [here](https://medium.com/google-exoplayer/exoplayer-2-x-mediasource-composition-6c285fcbca1f#.zfha8qupz). + * Looping support (see above) + ([#490](https://github.com/google/ExoPlayer/issues/490)). + * Ability to query information about all tracks in a piece of media (including + those not supported by the device) + ([#1121](https://github.com/google/ExoPlayer/issues/1121)). + * Improved player controls. + * Support for PSSH in fMP4 moof atoms + ([#1143](https://github.com/google/ExoPlayer/issues/1143)). + * Support for Opus in Ogg + ([#1447](https://github.com/google/ExoPlayer/issues/1447)). + * CacheDataSource support for standalone media file playbacks (mp3, mp4 etc). + * FFMPEG extension (for audio only). * Key bug fixes: - * Removed unnecessary secondary requests when playing standalone media - files ([#1041](https://github.com/google/ExoPlayer/issues/1041)). - * Fixed playback of video only (i.e. no audio) live streams - ([#758](https://github.com/google/ExoPlayer/issues/758)). - * Fixed silent failure when media buffer is too small - ([#583](https://github.com/google/ExoPlayer/issues/583)). - * Suppressed "Sending message to a Handler on a dead thread" warnings - ([#426](https://github.com/google/ExoPlayer/issues/426)). + * Removed unnecessary secondary requests when playing standalone media files + ([#1041](https://github.com/google/ExoPlayer/issues/1041)). + * Fixed playback of video only (i.e. no audio) live streams + ([#758](https://github.com/google/ExoPlayer/issues/758)). + * Fixed silent failure when media buffer is too small + ([#583](https://github.com/google/ExoPlayer/issues/583)). + * Suppressed "Sending message to a Handler on a dead thread" warnings + ([#426](https://github.com/google/ExoPlayer/issues/426)). # Legacy release notes # @@ -304,6 +335,12 @@ in all V2 releases. This cannot be assumed for changes in r1.5.12 and later, however it can be assumed that all such changes are included in the most recent V2 release. +### r1.5.16 ### + +* VP9 extension: Reduced binary size. +* FLAC extension: Enabled 64 bit targets and fixed proguard config. +* Misc bugfixes. + ### r1.5.15 ### * SmoothStreaming: Fixed handling of start_time placeholder diff --git a/build.gradle b/build.gradle index 9883c04e54..cbc34cecd6 100644 --- a/build.gradle +++ b/build.gradle @@ -19,8 +19,15 @@ buildscript { classpath 'com.android.tools.build:gradle:2.3.0' classpath 'com.novoda:bintray-release:0.4.0' } + // Workaround for the following test coverage issue. Remove when fixed: + // https://code.google.com/p/android/issues/detail?id=226070 + configurations.all { + resolutionStrategy { + force 'org.jacoco:org.jacoco.report:0.7.4.201502262128' + force 'org.jacoco:org.jacoco.core:0.7.4.201502262128' + } + } } - allprojects { repositories { jcenter() @@ -30,16 +37,26 @@ allprojects { // components provided by the library may be of use on older devices. // However, please note that the core media playback functionality // provided by the library requires API level 16 or greater. - minSdkVersion=9 - compileSdkVersion=25 - targetSdkVersion=25 - buildToolsVersion='25' + minSdkVersion = 9 + compileSdkVersion = 25 + targetSdkVersion = 25 + buildToolsVersion = '25' + testSupportLibraryVersion = '0.5' + supportLibraryVersion = '25.3.1' + dexmakerVersion = '1.2' + mockitoVersion = '1.9.5' releaseRepoName = getBintrayRepo() releaseUserOrg = 'google' releaseGroupId = 'com.google.android.exoplayer' - releaseVersion = 'r2.3.1' + releaseVersion = 'r2.4.0' releaseWebsite = 'https://github.com/google/ExoPlayer' } + if (it.hasProperty('externalBuildDir')) { + if (!new File(externalBuildDir).isAbsolute()) { + externalBuildDir = new File(rootDir, externalBuildDir) + } + buildDir = "${externalBuildDir}/${project.name}" + } } def getBintrayRepo() { @@ -47,3 +64,5 @@ def getBintrayRepo() { property('publicRepo').toBoolean() return publicRepo ? 'exoplayer' : 'exoplayer-test' } + +apply from: 'javadoc_combined.gradle' diff --git a/demo/build.gradle b/demo/build.gradle index 01946c8504..be5e52a25c 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -45,7 +45,11 @@ android { } dependencies { - compile project(':library') + compile project(':library-core') + compile project(':library-dash') + compile project(':library-hls') + compile project(':library-smoothstreaming') + compile project(':library-ui') withExtensionsCompile project(path: ':extension-ffmpeg') withExtensionsCompile project(path: ':extension-flac') withExtensionsCompile project(path: ':extension-opus') diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 9a6e1a4d3a..6580e687cc 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2400" + android:versionName="2.4.0"> diff --git a/demo/src/main/assets/media.exolist.json b/demo/src/main/assets/media.exolist.json index dd88f206c1..814c89a45b 100644 --- a/demo/src/main/assets/media.exolist.json +++ b/demo/src/main/assets/media.exolist.json @@ -416,13 +416,16 @@ ] }, { - "name": "Audio -> Video", + "name": "Audio -> Video -> Audio", "playlist": [ { "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4" }, { "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" + }, + { + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4" } ] }, diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java b/demo/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java new file mode 100644 index 0000000000..f9e9c34158 --- /dev/null +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java @@ -0,0 +1,86 @@ +/* + * 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.google.android.exoplayer2.demo; + +import android.text.TextUtils; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.util.MimeTypes; +import java.util.Locale; + +/** + * Utility methods for demo application. + */ +/*package*/ final class DemoUtil { + + /** + * Builds a track name for display. + * + * @param format {@link Format} of the track. + * @return a generated name specific to the track. + */ + public static String buildTrackName(Format format) { + String trackName; + if (MimeTypes.isVideo(format.sampleMimeType)) { + trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator( + buildResolutionString(format), buildBitrateString(format)), buildTrackIdString(format)), + buildSampleMimeTypeString(format)); + } else if (MimeTypes.isAudio(format.sampleMimeType)) { + trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(joinWithSeparator( + buildLanguageString(format), buildAudioPropertyString(format)), + buildBitrateString(format)), buildTrackIdString(format)), + buildSampleMimeTypeString(format)); + } else { + trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(buildLanguageString(format), + buildBitrateString(format)), buildTrackIdString(format)), + buildSampleMimeTypeString(format)); + } + return trackName.length() == 0 ? "unknown" : trackName; + } + + private static String buildResolutionString(Format format) { + return format.width == Format.NO_VALUE || format.height == Format.NO_VALUE + ? "" : format.width + "x" + format.height; + } + + private static String buildAudioPropertyString(Format format) { + return format.channelCount == Format.NO_VALUE || format.sampleRate == Format.NO_VALUE + ? "" : format.channelCount + "ch, " + format.sampleRate + "Hz"; + } + + private static String buildLanguageString(Format format) { + return TextUtils.isEmpty(format.language) || "und".equals(format.language) ? "" + : format.language; + } + + private static String buildBitrateString(Format format) { + return format.bitrate == Format.NO_VALUE ? "" + : String.format(Locale.US, "%.2fMbit", format.bitrate / 1000000f); + } + + private static String joinWithSeparator(String first, String second) { + return first.length() == 0 ? second : (second.length() == 0 ? first : first + ", " + second); + } + + private static String buildTrackIdString(Format format) { + return format.id == null ? "" : ("id:" + format.id); + } + + private static String buildSampleMimeTypeString(Format format) { + return format.sampleMimeType == null ? "" : format.sampleMimeType; + } + + private DemoUtil() {} +} diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index e39cd16743..953021fe6f 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.audio.AudioRendererEventListener; @@ -99,6 +100,12 @@ import java.util.Locale; Log.d(TAG, "positionDiscontinuity"); } + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + Log.d(TAG, "playbackParameters " + String.format( + "[speed=%.2f, pitch=%.2f]", playbackParameters.speed, playbackParameters.pitch)); + } + @Override public void onTimelineChanged(Timeline timeline, Object manifest) { int periodCount = timeline.getPeriodCount(); @@ -274,7 +281,7 @@ import java.util.Locale; @Override public void onRenderedFirstFrame(Surface surface) { - // Do nothing. + Log.d(TAG, "renderedFirstFrame [" + surface + "]"); } // DefaultDrmSessionManager.EventListener diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index adb04eaa24..2542f23e95 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -21,6 +21,7 @@ import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.support.annotation.NonNull; import android.text.TextUtils; import android.view.KeyEvent; import android.view.View; @@ -30,10 +31,11 @@ import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultLoadControl; +import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; @@ -111,6 +113,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay private TrackSelectionHelper trackSelectionHelper; private DebugTextViewHelper debugViewHelper; private boolean needRetrySource; + private TrackGroupArray lastSeenTrackGroupArray; private boolean shouldAutoPlay; private int resumeWindow; @@ -183,8 +186,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay } @Override - public void onRequestPermissionsResult(int requestCode, String[] permissions, - int[] grantResults) { + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { initializePlayer(); } else { @@ -250,17 +253,21 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay } } - @SimpleExoPlayer.ExtensionRendererMode int extensionRendererMode = + @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode = ((DemoApplication) getApplication()).useExtensionRenderers() - ? (preferExtensionDecoders ? SimpleExoPlayer.EXTENSION_RENDERER_MODE_PREFER - : SimpleExoPlayer.EXTENSION_RENDERER_MODE_ON) - : SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF; + ? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER + : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON) + : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF; + DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this, + drmSessionManager, extensionRendererMode); + TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(BANDWIDTH_METER); trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); trackSelectionHelper = new TrackSelectionHelper(trackSelector, videoTrackSelectionFactory); - player = ExoPlayerFactory.newSimpleInstance(this, trackSelector, new DefaultLoadControl(), - drmSessionManager, extensionRendererMode); + lastSeenTrackGroupArray = null; + + player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); player.addListener(this); eventLogger = new EventLogger(trackSelector); @@ -427,6 +434,11 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay } } + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + // Do nothing. + } + @Override public void onTimelineChanged(Timeline timeline, Object manifest) { // Do nothing. @@ -472,18 +484,22 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay } @Override + @SuppressWarnings("ReferenceEquality") public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { updateButtonVisibilities(); - MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); - if (mappedTrackInfo != null) { - if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_VIDEO) - == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { - showToast(R.string.error_unsupported_video); - } - if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_AUDIO) - == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { - showToast(R.string.error_unsupported_audio); + if (trackGroups != lastSeenTrackGroupArray) { + MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); + if (mappedTrackInfo != null) { + if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_VIDEO) + == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { + showToast(R.string.error_unsupported_video); + } + if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_AUDIO) + == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { + showToast(R.string.error_unsupported_audio); + } } + lastSeenTrackGroupArray = trackGroups; } } diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java b/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java index 576eb23c9d..033b515767 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java @@ -21,13 +21,11 @@ import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.res.TypedArray; -import android.text.TextUtils; import android.util.Pair; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CheckedTextView; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -37,9 +35,7 @@ import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedT import com.google.android.exoplayer2.trackselection.MappingTrackSelector.SelectionOverride; import com.google.android.exoplayer2.trackselection.RandomTrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.util.MimeTypes; import java.util.Arrays; -import java.util.Locale; /** * Helper class for displaying track selection dialogs. @@ -157,7 +153,7 @@ import java.util.Locale; CheckedTextView trackView = (CheckedTextView) inflater.inflate( trackViewLayoutId, root, false); trackView.setBackgroundResource(selectableItemBackgroundResourceId); - trackView.setText(buildTrackName(group.getFormat(trackIndex))); + trackView.setText(DemoUtil.buildTrackName(group.getFormat(trackIndex))); if (trackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex) == RendererCapabilities.FORMAT_HANDLED) { trackView.setFocusable(true); @@ -296,57 +292,4 @@ import java.util.Locale; return tracks; } - // Track name construction. - - private static String buildTrackName(Format format) { - String trackName; - if (MimeTypes.isVideo(format.sampleMimeType)) { - trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator( - buildResolutionString(format), buildBitrateString(format)), buildTrackIdString(format)), - buildSampleMimeTypeString(format)); - } else if (MimeTypes.isAudio(format.sampleMimeType)) { - trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(joinWithSeparator( - buildLanguageString(format), buildAudioPropertyString(format)), - buildBitrateString(format)), buildTrackIdString(format)), - buildSampleMimeTypeString(format)); - } else { - trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(buildLanguageString(format), - buildBitrateString(format)), buildTrackIdString(format)), - buildSampleMimeTypeString(format)); - } - return trackName.length() == 0 ? "unknown" : trackName; - } - - private static String buildResolutionString(Format format) { - return format.width == Format.NO_VALUE || format.height == Format.NO_VALUE - ? "" : format.width + "x" + format.height; - } - - private static String buildAudioPropertyString(Format format) { - return format.channelCount == Format.NO_VALUE || format.sampleRate == Format.NO_VALUE - ? "" : format.channelCount + "ch, " + format.sampleRate + "Hz"; - } - - private static String buildLanguageString(Format format) { - return TextUtils.isEmpty(format.language) || "und".equals(format.language) ? "" - : format.language; - } - - private static String buildBitrateString(Format format) { - return format.bitrate == Format.NO_VALUE ? "" - : String.format(Locale.US, "%.2fMbit", format.bitrate / 1000000f); - } - - private static String joinWithSeparator(String first, String second) { - return first.length() == 0 ? second : (second.length() == 0 ? first : first + ", " + second); - } - - private static String buildTrackIdString(Format format) { - return format.id == null ? "" : ("id:" + format.id); - } - - private static String buildSampleMimeTypeString(Format format) { - return format.sampleMimeType == null ? "" : format.sampleMimeType; - } - } diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index f031a9dc48..5611817b2e 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -29,13 +29,18 @@ android { } dependencies { - compile project(':library') + compile project(':library-core') compile files('libs/cronet_api.jar') compile files('libs/cronet_impl_common_java.jar') compile files('libs/cronet_impl_native_java.jar') - androidTestCompile 'com.google.dexmaker:dexmaker:1.2' - androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2' - androidTestCompile 'org.mockito:mockito-core:1.9.5' androidTestCompile project(':library') - androidTestCompile 'com.android.support.test:runner:0.5' + androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion + androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion + androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion + androidTestCompile 'com.android.support.test:runner:' + testSupportLibraryVersion } + +ext { + javadocTitle = 'Cronet extension' +} +apply from: '../../javadoc_library.gradle' diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java index db560305a7..2ad6da6a54 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java @@ -72,7 +72,7 @@ public final class CronetDataSourceFactory extends BaseFactory { protected CronetDataSource createDataSourceInternal(HttpDataSource.RequestProperties defaultRequestProperties) { return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener, - connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, null, defaultRequestProperties); + connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, defaultRequestProperties); } } diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index beafcb6a96..4ce9173ec9 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -31,21 +31,18 @@ FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main" NDK_PATH="" ``` -* Fetch and build FFmpeg. For example, to fetch and build for armv7a: +* Set up host platform ("darwin-x86_64" for Mac OS X): ``` -cd "${FFMPEG_EXT_PATH}/jni" && \ -git clone git://source.ffmpeg.org/ffmpeg ffmpeg && cd ffmpeg && \ -./configure \ - --libdir=android-libs/armeabi-v7a \ - --arch=arm \ - --cpu=armv7-a \ - --cross-prefix="${NDK_PATH}/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-" \ +HOST_PLATFORM="linux-x86_64" +``` + +* Fetch and build FFmpeg. For example, to fetch and build for armeabi-v7a, + arm64-v8a and x86 on Linux x86_64: + +``` +COMMON_OPTIONS="\ --target-os=android \ - --sysroot="${NDK_PATH}/platforms/android-9/arch-arm/" \ - --extra-cflags="-march=armv7-a -mfloat-abi=softfp" \ - --extra-ldflags="-Wl,--fix-cortex-a8" \ - --extra-ldexeflags=-pie \ --disable-static \ --enable-shared \ --disable-doc \ @@ -57,22 +54,56 @@ git clone git://source.ffmpeg.org/ffmpeg ffmpeg && cd ffmpeg && \ --disable-postproc \ --disable-avfilter \ --disable-symver \ + --disable-swresample \ --enable-avresample \ --enable-decoder=vorbis \ --enable-decoder=opus \ --enable-decoder=flac \ - --enable-decoder=alac \ + " && \ +cd "${FFMPEG_EXT_PATH}/jni" && \ +git clone git://source.ffmpeg.org/ffmpeg ffmpeg && cd ffmpeg && \ +./configure \ + --libdir=android-libs/armeabi-v7a \ + --arch=arm \ + --cpu=armv7-a \ + --cross-prefix="${NDK_PATH}/toolchains/arm-linux-androideabi-4.9/prebuilt/${HOST_PLATFORM}/bin/arm-linux-androideabi-" \ + --sysroot="${NDK_PATH}/platforms/android-9/arch-arm/" \ + --extra-cflags="-march=armv7-a -mfloat-abi=softfp" \ + --extra-ldflags="-Wl,--fix-cortex-a8" \ + --extra-ldexeflags=-pie \ + ${COMMON_OPTIONS} \ && \ -make -j4 && \ -make install-libs +make -j4 && make install-libs && \ +make clean && ./configure \ + --libdir=android-libs/arm64-v8a \ + --arch=aarch64 \ + --cpu=armv8-a \ + --cross-prefix="${NDK_PATH}/toolchains/aarch64-linux-android-4.9/prebuilt/${HOST_PLATFORM}/bin/aarch64-linux-android-" \ + --sysroot="${NDK_PATH}/platforms/android-21/arch-arm64/" \ + --extra-ldexeflags=-pie \ + ${COMMON_OPTIONS} \ + && \ +make -j4 && make install-libs && \ +make clean && ./configure \ + --libdir=android-libs/x86 \ + --arch=x86 \ + --cpu=i686 \ + --cross-prefix="${NDK_PATH}/toolchains/x86-4.9/prebuilt/${HOST_PLATFORM}/bin/i686-linux-android-" \ + --sysroot="${NDK_PATH}/platforms/android-9/arch-x86/" \ + --extra-ldexeflags=-pie \ + --disable-asm \ + ${COMMON_OPTIONS} \ + && \ +make -j4 && make install-libs && \ +make clean ``` -* Build the JNI native libraries. Repeat this step for any other architectures - you need to support. +* Build the JNI native libraries, setting `APP_ABI` to include the architectures + built in the previous step. For example: ``` cd "${FFMPEG_EXT_PATH}"/jni && \ -${NDK_PATH}/ndk-build APP_ABI=armeabi-v7a -j4 +${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86" -j4 ``` * In your project, you can add a dependency on the extension by using a rule diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle index a6523788cb..0eddd017a4 100644 --- a/extensions/ffmpeg/build.gradle +++ b/extensions/ffmpeg/build.gradle @@ -30,5 +30,10 @@ android { } dependencies { - compile project(':library') + compile project(':library-core') } + +ext { + javadocTitle = 'FFmpeg extension' +} +apply from: '../../javadoc_library.gradle' diff --git a/extensions/ffmpeg/src/main/jni/Android.mk b/extensions/ffmpeg/src/main/jni/Android.mk index f435ab7e98..046f90a5b2 100644 --- a/extensions/ffmpeg/src/main/jni/Android.mk +++ b/extensions/ffmpeg/src/main/jni/Android.mk @@ -31,15 +31,10 @@ LOCAL_MODULE := libavresample LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so include $(PREBUILT_SHARED_LIBRARY) -include $(CLEAR_VARS) -LOCAL_MODULE := libswresample -LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so -include $(PREBUILT_SHARED_LIBRARY) - include $(CLEAR_VARS) LOCAL_MODULE := ffmpeg LOCAL_SRC_FILES := ffmpeg_jni.cc LOCAL_C_INCLUDES := ffmpeg -LOCAL_SHARED_LIBRARIES := libavcodec libavresample libavutil libswresample +LOCAL_SHARED_LIBRARIES := libavcodec libavresample libavutil LOCAL_LDLIBS := -Lffmpeg/android-libs/$(TARGET_ARCH_ABI) -llog include $(BUILD_SHARED_LIBRARY) diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle index 1c23b9987c..4a6b8e0e5a 100644 --- a/extensions/flac/build.gradle +++ b/extensions/flac/build.gradle @@ -30,7 +30,11 @@ android { } dependencies { - compile project(':library') + compile project(':library-core') androidTestCompile project(':testutils') } +ext { + javadocTitle = 'FLAC extension' +} +apply from: '../../javadoc_library.gradle' diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java index 990c470a93..21f01f0cca 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java @@ -22,6 +22,7 @@ import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; @@ -102,6 +103,11 @@ public class FlacPlaybackTest extends InstrumentationTestCase { // Do nothing. } + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + // Do nothing. + } + @Override public void onTimelineChanged(Timeline timeline, Object manifest) { // Do nothing. diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle index 5156cf0540..f622a73758 100644 --- a/extensions/gvr/build.gradle +++ b/extensions/gvr/build.gradle @@ -24,10 +24,15 @@ android { } dependencies { - compile project(':library') + compile project(':library-core') compile 'com.google.vr:sdk-audio:1.30.0' } +ext { + javadocTitle = 'GVR extension' +} +apply from: '../../javadoc_library.gradle' + ext { releaseArtifact = 'extension-gvr' releaseDescription = 'Google VR extension for ExoPlayer.' diff --git a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java index 2117985da0..980424904d 100644 --- a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java +++ b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java @@ -152,14 +152,19 @@ public final class GvrAudioProcessor implements AudioProcessor { @Override public void flush() { - gvrAudioSurround.flush(); + if (gvrAudioSurround != null) { + gvrAudioSurround.flush(); + } inputEnded = false; } @Override - public synchronized void release() { - buffer = null; + public synchronized void reset() { maybeReleaseGvrAudioSurround(); + inputEnded = false; + buffer = null; + sampleRateHz = Format.NO_VALUE; + channelCount = Format.NO_VALUE; } private void maybeReleaseGvrAudioSurround() { diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index 3a2daefb8f..f47f1a8556 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -29,12 +29,17 @@ android { } dependencies { - compile project(':library') + compile project(':library-core') compile('com.squareup.okhttp3:okhttp:3.6.0') { exclude group: 'org.json' } } +ext { + javadocTitle = 'OkHttp extension' +} +apply from: '../../javadoc_library.gradle' + ext { releaseArtifact = 'extension-okhttp' releaseDescription = 'OkHttp extension for ExoPlayer.' diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle index a6523788cb..31d5450fdd 100644 --- a/extensions/opus/build.gradle +++ b/extensions/opus/build.gradle @@ -30,5 +30,10 @@ android { } dependencies { - compile project(':library') + compile project(':library-core') } + +ext { + javadocTitle = 'Opus extension' +} +apply from: '../../javadoc_library.gradle' diff --git a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java index 3e07186995..263934d982 100644 --- a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java +++ b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java @@ -22,6 +22,7 @@ import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; @@ -102,6 +103,11 @@ public class OpusPlaybackTest extends InstrumentationTestCase { // Do nothing. } + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + // Do nothing. + } + @Override public void onTimelineChanged(Timeline timeline, Object manifest) { // Do nothing. diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java index 83e461d279..95c38c34bb 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java @@ -161,7 +161,7 @@ import java.util.List; cryptoInfo.key, cryptoInfo.iv, cryptoInfo.numSubSamples, cryptoInfo.numBytesOfClearData, cryptoInfo.numBytesOfEncryptedData) : opusDecode(nativeDecoderContext, inputBuffer.timeUs, inputData, inputData.limit(), - outputBuffer, SAMPLE_RATE); + outputBuffer); if (result < 0) { if (result == DRM_ERROR) { String message = "Drm error: " + opusGetErrorMessage(nativeDecoderContext); @@ -210,7 +210,7 @@ import java.util.List; private native long opusInit(int sampleRate, int channelCount, int numStreams, int numCoupled, int gain, byte[] streamMap); private native int opusDecode(long decoder, long timeUs, ByteBuffer inputBuffer, int inputSize, - SimpleOutputBuffer outputBuffer, int sampleRate); + SimpleOutputBuffer outputBuffer); private native int opusSecureDecode(long decoder, long timeUs, ByteBuffer inputBuffer, int inputSize, SimpleOutputBuffer outputBuffer, int sampleRate, ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv, diff --git a/extensions/opus/src/main/jni/opus_jni.cc b/extensions/opus/src/main/jni/opus_jni.cc index 48c1bd5e6d..8d9c1a4152 100644 --- a/extensions/opus/src/main/jni/opus_jni.cc +++ b/extensions/opus/src/main/jni/opus_jni.cc @@ -59,6 +59,7 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { } static const int kBytesPerSample = 2; // opus fixed point uses 16 bit samples. +static const int kMaxOpusOutputPacketSizeSamples = 960 * 6; static int channelCount; static int errorCode; @@ -92,16 +93,14 @@ DECODER_FUNC(jlong, opusInit, jint sampleRate, jint channelCount, } DECODER_FUNC(jint, opusDecode, jlong jDecoder, jlong jTimeUs, - jobject jInputBuffer, jint inputSize, jobject jOutputBuffer, - jint sampleRate) { + jobject jInputBuffer, jint inputSize, jobject jOutputBuffer) { OpusMSDecoder* decoder = reinterpret_cast(jDecoder); const uint8_t* inputBuffer = reinterpret_cast( env->GetDirectBufferAddress(jInputBuffer)); - const int32_t inputSampleCount = - opus_packet_get_nb_samples(inputBuffer, inputSize, sampleRate); - const jint outputSize = inputSampleCount * kBytesPerSample * channelCount; + const jint outputSize = + kMaxOpusOutputPacketSizeSamples * kBytesPerSample * channelCount; env->CallObjectMethod(jOutputBuffer, outputBufferInit, jTimeUs, outputSize); const jobject jOutputBufferData = env->CallObjectMethod(jOutputBuffer, @@ -110,7 +109,7 @@ DECODER_FUNC(jint, opusDecode, jlong jDecoder, jlong jTimeUs, int16_t* outputBufferData = reinterpret_cast( env->GetDirectBufferAddress(jOutputBufferData)); int sampleCount = opus_multistream_decode(decoder, inputBuffer, inputSize, - outputBufferData, outputSize, 0); + outputBufferData, kMaxOpusOutputPacketSizeSamples, 0); // record error code errorCode = (sampleCount < 0) ? sampleCount : 0; return (sampleCount < 0) ? sampleCount diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 90ded8fdc0..53ef4b0bfd 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -49,7 +49,7 @@ git clone https://chromium.googlesource.com/libyuv/libyuv libyuv cd "${VP9_EXT_PATH}/jni/libvpx" && \ git checkout tags/v1.6.1 -b v1.6.1 && \ cd "${VP9_EXT_PATH}/jni/libyuv" && \ -git checkout e2611a73 +git checkout 996a2bbd ``` * Run a script that generates necessary configuration files for libvpx: diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle index 91d80f4970..5068586a4a 100644 --- a/extensions/vp9/build.gradle +++ b/extensions/vp9/build.gradle @@ -30,6 +30,10 @@ android { } dependencies { - compile project(':library') + compile project(':library-core') } +ext { + javadocTitle = 'VP9 extension' +} +apply from: '../../javadoc_library.gradle' diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java index f888554e22..2647776b74 100644 --- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java +++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java @@ -23,6 +23,7 @@ import android.util.Log; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; @@ -134,6 +135,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase { // Do nothing. } + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + // Do nothing. + } + @Override public void onTimelineChanged(Timeline timeline, Object manifest) { // Do nothing. diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java index d0417bc37e..c44c703bb1 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java @@ -87,8 +87,8 @@ public final class LibvpxVideoRenderer extends BaseRenderer { private boolean inputStreamEnded; private boolean outputStreamEnded; - private int lastReportedWidth; - private int lastReportedHeight; + private int reportedWidth; + private int reportedHeight; private long droppedFrameAccumulationStartTimeMs; private int droppedFrames; @@ -147,10 +147,10 @@ public final class LibvpxVideoRenderer extends BaseRenderer { this.maxDroppedFramesToNotify = maxDroppedFramesToNotify; this.drmSessionManager = drmSessionManager; this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; - joiningDeadlineMs = -1; - clearLastReportedVideoSize(); + joiningDeadlineMs = C.TIME_UNSET; + clearReportedVideoSize(); formatHolder = new FormatHolder(); - flagsOnlyBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); + flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance(); eventDispatcher = new EventDispatcher(eventHandler, eventListener); outputMode = VpxDecoder.OUTPUT_MODE_NONE; } @@ -185,44 +185,40 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } } - if (isRendererAvailable()) { - drmSession = pendingDrmSession; - ExoMediaCrypto mediaCrypto = null; - if (drmSession != null) { - int drmSessionState = drmSession.getState(); - if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); - } else if (drmSessionState == DrmSession.STATE_OPENED - || drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) { - mediaCrypto = drmSession.getMediaCrypto(); - } else { - // The drm session isn't open yet. - return; - } + // We have a format. + drmSession = pendingDrmSession; + ExoMediaCrypto mediaCrypto = null; + if (drmSession != null) { + int drmSessionState = drmSession.getState(); + if (drmSessionState == DrmSession.STATE_ERROR) { + throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); + } else if (drmSessionState == DrmSession.STATE_OPENED + || drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) { + mediaCrypto = drmSession.getMediaCrypto(); + } else { + // The drm session isn't open yet. + return; } - try { - if (decoder == null) { - // If we don't have a decoder yet, we need to instantiate one. - long codecInitializingTimestamp = SystemClock.elapsedRealtime(); - TraceUtil.beginSection("createVpxDecoder"); - decoder = new VpxDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, - mediaCrypto); - decoder.setOutputMode(outputMode); - TraceUtil.endSection(); - long codecInitializedTimestamp = SystemClock.elapsedRealtime(); - eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp, - codecInitializedTimestamp - codecInitializingTimestamp); - decoderCounters.decoderInitCount++; - } - TraceUtil.beginSection("drainAndFeed"); - while (drainOutputBuffer(positionUs)) {} - while (feedInputBuffer()) {} + } + try { + if (decoder == null) { + // If we don't have a decoder yet, we need to instantiate one. + long codecInitializingTimestamp = SystemClock.elapsedRealtime(); + TraceUtil.beginSection("createVpxDecoder"); + decoder = new VpxDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, mediaCrypto); + decoder.setOutputMode(outputMode); TraceUtil.endSection(); - } catch (VpxDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + long codecInitializedTimestamp = SystemClock.elapsedRealtime(); + eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp, + codecInitializedTimestamp - codecInitializingTimestamp); + decoderCounters.decoderInitCount++; } - } else { - skipToKeyframeBefore(positionUs); + TraceUtil.beginSection("drainAndFeed"); + while (drainOutputBuffer(positionUs)) {} + while (feedInputBuffer()) {} + TraceUtil.endSection(); + } catch (VpxDecoderException e) { + throw ExoPlaybackException.createForRenderer(e, getIndex()); } decoderCounters.ensureUpdated(); } @@ -257,27 +253,26 @@ public final class LibvpxVideoRenderer extends BaseRenderer { return false; } - // Drop the frame if we're joining and are more than 30ms late, or if we have the next frame - // and that's also late. Else we'll render what we have. - if ((joiningDeadlineMs != -1 && outputBuffer.timeUs < positionUs - 30000) - || (nextOutputBuffer != null && !nextOutputBuffer.isEndOfStream() - && nextOutputBuffer.timeUs < positionUs)) { - decoderCounters.droppedOutputBufferCount++; - droppedFrames++; - consecutiveDroppedFrameCount++; - decoderCounters.maxConsecutiveDroppedOutputBufferCount = Math.max( - consecutiveDroppedFrameCount, - decoderCounters.maxConsecutiveDroppedOutputBufferCount); - if (droppedFrames == maxDroppedFramesToNotify) { - maybeNotifyDroppedFrames(); + if (outputMode == VpxDecoder.OUTPUT_MODE_NONE) { + // Skip frames in sync with playback, so we'll be at the right frame if the mode changes. + if (outputBuffer.timeUs <= positionUs) { + skipBuffer(); + return true; } - outputBuffer.release(); - outputBuffer = null; + return false; + } + + final long nextOutputBufferTimeUs = + nextOutputBuffer != null && !nextOutputBuffer.isEndOfStream() + ? nextOutputBuffer.timeUs : C.TIME_UNSET; + if (shouldDropOutputBuffer( + outputBuffer.timeUs, nextOutputBufferTimeUs, positionUs, joiningDeadlineMs)) { + dropBuffer(); return true; } - // If we have not rendered any frame so far (either initially or immediately following a seek), - // render one frame irrespective of the state or current position. + // If we have yet to render a frame to the current output (either initially or immediately + // following a seek), render one irrespective of the state or current position. if (!renderedFirstFrame || (getState() == STATE_STARTED && outputBuffer.timeUs <= positionUs + 30000)) { renderBuffer(); @@ -285,27 +280,63 @@ public final class LibvpxVideoRenderer extends BaseRenderer { return false; } + + /** + * Returns whether the current frame should be dropped. + * + * @param outputBufferTimeUs The timestamp of the current output buffer. + * @param nextOutputBufferTimeUs The timestamp of the next output buffer or + * {@link TIME_UNSET} if the next output buffer is unavailable. + * @param positionUs The current playback position. + * @param joiningDeadlineMs The joining deadline. + * @return Returns whether to drop the current output buffer. + */ + protected boolean shouldDropOutputBuffer(long outputBufferTimeUs, long nextOutputBufferTimeUs, + long positionUs, long joiningDeadlineMs) { + // Drop the frame if we're joining and are more than 30ms late, or if we have the next frame + // and that's also late. Else we'll render what we have. + return (joiningDeadlineMs != C.TIME_UNSET && outputBufferTimeUs < positionUs - 30000) + || (nextOutputBufferTimeUs != C.TIME_UNSET && nextOutputBufferTimeUs < positionUs); + } + private void renderBuffer() { - decoderCounters.renderedOutputBufferCount++; - consecutiveDroppedFrameCount = 0; - maybeNotifyVideoSizeChanged(outputBuffer.width, outputBuffer.height); - if (outputBuffer.mode == VpxDecoder.OUTPUT_MODE_RGB && surface != null) { - renderRgbFrame(outputBuffer, scaleToFit); - if (!renderedFirstFrame) { - renderedFirstFrame = true; - eventDispatcher.renderedFirstFrame(surface); - } - outputBuffer.release(); - } else if (outputBuffer.mode == VpxDecoder.OUTPUT_MODE_YUV && outputBufferRenderer != null) { - // The renderer will release the buffer. - outputBufferRenderer.setOutputBuffer(outputBuffer); - if (!renderedFirstFrame) { - renderedFirstFrame = true; - eventDispatcher.renderedFirstFrame(null); - } + int bufferMode = outputBuffer.mode; + boolean renderRgb = bufferMode == VpxDecoder.OUTPUT_MODE_RGB && surface != null; + boolean renderYuv = bufferMode == VpxDecoder.OUTPUT_MODE_YUV && outputBufferRenderer != null; + if (!renderRgb && !renderYuv) { + dropBuffer(); } else { - outputBuffer.release(); + maybeNotifyVideoSizeChanged(outputBuffer.width, outputBuffer.height); + if (renderRgb) { + renderRgbFrame(outputBuffer, scaleToFit); + outputBuffer.release(); + } else /* renderYuv */ { + outputBufferRenderer.setOutputBuffer(outputBuffer); + // The renderer will release the buffer. + } + outputBuffer = null; + consecutiveDroppedFrameCount = 0; + decoderCounters.renderedOutputBufferCount++; + maybeNotifyRenderedFirstFrame(); } + } + + private void dropBuffer() { + decoderCounters.droppedOutputBufferCount++; + droppedFrames++; + consecutiveDroppedFrameCount++; + decoderCounters.maxConsecutiveDroppedOutputBufferCount = Math.max( + consecutiveDroppedFrameCount, decoderCounters.maxConsecutiveDroppedOutputBufferCount); + if (droppedFrames == maxDroppedFramesToNotify) { + maybeNotifyDroppedFrames(); + } + outputBuffer.release(); + outputBuffer = null; + } + + private void skipBuffer() { + decoderCounters.skippedOutputBufferCount++; + outputBuffer.release(); outputBuffer = null; } @@ -406,11 +437,11 @@ public final class LibvpxVideoRenderer extends BaseRenderer { return false; } if (format != null && (isSourceReady() || outputBuffer != null) - && (renderedFirstFrame || !isRendererAvailable())) { + && (renderedFirstFrame || outputMode == VpxDecoder.OUTPUT_MODE_NONE)) { // Ready. If we were joining then we've now joined, so clear the joining deadline. - joiningDeadlineMs = -1; + joiningDeadlineMs = C.TIME_UNSET; return true; - } else if (joiningDeadlineMs == -1) { + } else if (joiningDeadlineMs == C.TIME_UNSET) { // Not joining. return false; } else if (SystemClock.elapsedRealtime() < joiningDeadlineMs) { @@ -418,7 +449,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { return true; } else { // The joining deadline has been exceeded. Give up and clear the deadline. - joiningDeadlineMs = -1; + joiningDeadlineMs = C.TIME_UNSET; return false; } } @@ -433,24 +464,27 @@ public final class LibvpxVideoRenderer extends BaseRenderer { protected void onPositionReset(long positionUs, boolean joining) { inputStreamEnded = false; outputStreamEnded = false; - renderedFirstFrame = false; + clearRenderedFirstFrame(); consecutiveDroppedFrameCount = 0; if (decoder != null) { flushDecoder(); } - joiningDeadlineMs = joining && allowedJoiningTimeMs > 0 - ? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : -1; + if (joining) { + setJoiningDeadlineMs(); + } else { + joiningDeadlineMs = C.TIME_UNSET; + } } @Override protected void onStarted() { droppedFrames = 0; droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime(); + joiningDeadlineMs = C.TIME_UNSET; } @Override protected void onStopped() { - joiningDeadlineMs = -1; maybeNotifyDroppedFrames(); } @@ -460,7 +494,8 @@ public final class LibvpxVideoRenderer extends BaseRenderer { outputBuffer = null; format = null; waitingForKeys = false; - clearLastReportedVideoSize(); + clearReportedVideoSize(); + clearRenderedFirstFrame(); try { releaseDecoder(); } finally { @@ -537,47 +572,78 @@ public final class LibvpxVideoRenderer extends BaseRenderer { private void setOutput(Surface surface, VpxOutputBufferRenderer outputBufferRenderer) { // At most one output may be non-null. Both may be null if the output is being cleared. Assertions.checkState(surface == null || outputBufferRenderer == null); - // Clear state so that we always call the event listener with the video size and when a frame - // is rendered, even if the output hasn't changed. - renderedFirstFrame = false; - clearLastReportedVideoSize(); - // We only need to update the decoder if the output has changed. if (this.surface != surface || this.outputBufferRenderer != outputBufferRenderer) { + // The output has changed. this.surface = surface; this.outputBufferRenderer = outputBufferRenderer; outputMode = outputBufferRenderer != null ? VpxDecoder.OUTPUT_MODE_YUV : surface != null ? VpxDecoder.OUTPUT_MODE_RGB : VpxDecoder.OUTPUT_MODE_NONE; - updateDecoder(); - } - } - - private void updateDecoder() { - if (decoder != null) { - if (outputMode == VpxDecoder.OUTPUT_MODE_NONE) { - releaseDecoder(); + if (outputMode != VpxDecoder.OUTPUT_MODE_NONE) { + if (decoder != null) { + decoder.setOutputMode(outputMode); + } + // If we know the video size, report it again immediately. + maybeRenotifyVideoSizeChanged(); + // We haven't rendered to the new output yet. + clearRenderedFirstFrame(); + if (getState() == STATE_STARTED) { + setJoiningDeadlineMs(); + } } else { - decoder.setOutputMode(outputMode); + // The output has been removed. We leave the outputMode of the underlying decoder unchanged + // in anticipation that a subsequent output will likely be of the same type. + clearReportedVideoSize(); + clearRenderedFirstFrame(); } + } else if (outputMode != VpxDecoder.OUTPUT_MODE_NONE) { + // The output is unchanged and non-null. If we know the video size and/or have already + // rendered to the output, report these again immediately. + maybeRenotifyVideoSizeChanged(); + maybeRenotifyRenderedFirstFrame(); } } - private boolean isRendererAvailable() { - return surface != null || outputBufferRenderer != null; + private void setJoiningDeadlineMs() { + joiningDeadlineMs = allowedJoiningTimeMs > 0 + ? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET; } - private void clearLastReportedVideoSize() { - lastReportedWidth = Format.NO_VALUE; - lastReportedHeight = Format.NO_VALUE; + private void clearRenderedFirstFrame() { + renderedFirstFrame = false; + } + + private void maybeNotifyRenderedFirstFrame() { + if (!renderedFirstFrame) { + renderedFirstFrame = true; + eventDispatcher.renderedFirstFrame(surface); + } + } + + private void maybeRenotifyRenderedFirstFrame() { + if (renderedFirstFrame) { + eventDispatcher.renderedFirstFrame(surface); + } + } + + private void clearReportedVideoSize() { + reportedWidth = Format.NO_VALUE; + reportedHeight = Format.NO_VALUE; } private void maybeNotifyVideoSizeChanged(int width, int height) { - if (lastReportedWidth != width || lastReportedHeight != height) { - lastReportedWidth = width; - lastReportedHeight = height; + if (reportedWidth != width || reportedHeight != height) { + reportedWidth = width; + reportedHeight = height; eventDispatcher.videoSizeChanged(width, height, 0, 1); } } + private void maybeRenotifyVideoSizeChanged() { + if (reportedWidth != Format.NO_VALUE || reportedHeight != Format.NO_VALUE) { + eventDispatcher.videoSizeChanged(reportedWidth, reportedHeight, 0, 1); + } + } + private void maybeNotifyDroppedFrames() { if (droppedFrames > 0) { long now = SystemClock.elapsedRealtime(); diff --git a/extensions/vp9/src/main/jni/Android.mk b/extensions/vp9/src/main/jni/Android.mk index 8ad32a254a..92fed0a064 100644 --- a/extensions/vp9/src/main/jni/Android.mk +++ b/extensions/vp9/src/main/jni/Android.mk @@ -21,6 +21,7 @@ LIBYUV_ROOT := $(WORKING_DIR)/libyuv # build libyuv_static.a LOCAL_PATH := $(WORKING_DIR) +LIBYUV_DISABLE_JPEG := "yes" include $(LIBYUV_ROOT)/Android.mk # build libvpx.so diff --git a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh index 566396e0bf..15dbabdb1f 100755 --- a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh +++ b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh @@ -68,7 +68,7 @@ limit=$((${#arch[@]} - 1)) # everything else will be removed. allowed_files="libvpx_srcs.txt vpx_config.c vpx_config.h vpx_scale_rtcd.h" allowed_files+=" vp8_rtcd.h vp9_rtcd.h vpx_version.h vpx_config.asm" -allowed_files+=" vpx_dsp_rtcd.h" +allowed_files+=" vpx_dsp_rtcd.h libvpx.ver" remove_trailing_whitespace() { perl -pi -e 's/\s\+$//' "$@" diff --git a/extensions/vp9/src/main/jni/libvpx.mk b/extensions/vp9/src/main/jni/libvpx.mk index 887de56218..005f49fcc6 100644 --- a/extensions/vp9/src/main/jni/libvpx.mk +++ b/extensions/vp9/src/main/jni/libvpx.mk @@ -37,9 +37,7 @@ LOCAL_SRC_FILES += $(addprefix libvpx/, $(filter-out vpx_config.c, \ # include assembly files if they exist # "%.asm.[sS]" covers neon assembly and "%.asm" covers x86 assembly LOCAL_SRC_FILES += $(addprefix libvpx/, \ - $(filter %.asm.s %.asm, $(libvpx_codec_srcs))) -LOCAL_SRC_FILES += $(addprefix libvpx/, \ - $(filter %.asm.S %.asm, $(libvpx_codec_srcs))) + $(filter %.asm.s %.asm.S %.asm, $(libvpx_codec_srcs))) ifneq ($(findstring armeabi-v7a, $(TARGET_ARCH_ABI)),) # append .neon to *_neon.c and *.[sS] @@ -48,10 +46,8 @@ LOCAL_SRC_FILES := $(subst .s,.s.neon,$(LOCAL_SRC_FILES)) LOCAL_SRC_FILES := $(subst .S,.S.neon,$(LOCAL_SRC_FILES)) endif -# remove duplicates -LOCAL_SRC_FILES := $(sort $(LOCAL_SRC_FILES)) - LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/libvpx \ $(LOCAL_PATH)/libvpx/vpx +LOCAL_LDFLAGS := -Wl,--version-script=$(CONFIG_DIR)/libvpx.ver include $(BUILD_SHARED_LIBRARY) diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc index c091091389..d02d524713 100644 --- a/extensions/vp9/src/main/jni/vpx_jni.cc +++ b/extensions/vp9/src/main/jni/vpx_jni.cc @@ -20,8 +20,9 @@ #include #include -#include #include +#include +#include #include #include "libyuv.h" // NOLINT diff --git a/javadoc_combined.gradle b/javadoc_combined.gradle new file mode 100644 index 0000000000..1fec48ca25 --- /dev/null +++ b/javadoc_combined.gradle @@ -0,0 +1,68 @@ +// 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. +class CombinedJavadocPlugin implements Plugin { + + static final String TASK_NAME = "generateCombinedJavadoc" + + @Override + void apply(Project project) { + project.gradle.projectsEvaluated { + Set libraryModules = getLibraryModules(project) + if (!libraryModules.isEmpty()) { + String sdkDirectory = getSdkDirectory(libraryModules) + project.task(TASK_NAME, type: Javadoc) { + description = "Generates combined Javadoc." + title = "ExoPlayer library" + source = libraryModules.generateJavadoc.source + classpath = project.files(libraryModules.generateJavadoc.classpath) + destinationDir = project.file("$project.buildDir/docs/javadoc") + options { + links "http://docs.oracle.com/javase/7/docs/api/" + linksOffline "https://developer.android.com/reference", + "${sdkDirectory}/docs/reference" + encoding = "UTF-8" + } + exclude "**/BuildConfig.java" + exclude "**/R.java" + destinationDir project.file("$project.buildDir/docs/javadoc") + doLast { + libraryModules.each { libraryModule -> + project.copy { + from "${libraryModule.projectDir}/src/main/javadoc" + into "${project.buildDir}/docs/javadoc" + } + } + } + } + } + } + } + + // Returns Android library modules that declare a generateJavadoc task. + private Set getLibraryModules(Project project) { + project.subprojects.findAll { + it.plugins.findPlugin("com.android.library") && + it.tasks.findByName("generateJavadoc") + } + } + + // Returns the Android SDK directory given a set of Android library modules. + private String getSdkDirectory(Set libraryModules) { + // We can retrieve the Android SDK directory from any module. + return libraryModules.iterator().next().android.sdkDirectory + } + +} + +apply plugin: CombinedJavadocPlugin diff --git a/javadoc_library.gradle b/javadoc_library.gradle new file mode 100644 index 0000000000..ea193e661c --- /dev/null +++ b/javadoc_library.gradle @@ -0,0 +1,40 @@ +// 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. +android.libraryVariants.all { variant -> + def name = variant.buildType.name + if (!name.equals("release")) { + return; // Skip non-release builds. + } + task("generateJavadoc", type: Javadoc) { + description = "Generates Javadoc for the ${javadocTitle}." + title = "ExoPlayer ${javadocTitle}" + source = variant.javaCompile.source + classpath = files(variant.javaCompile.classpath.files, + project.android.getBootClasspath()) + options { + links "http://docs.oracle.com/javase/7/docs/api/" + linksOffline "https://developer.android.com/reference", + "${android.sdkDirectory}/docs/reference" + encoding = "UTF-8" + } + exclude "**/BuildConfig.java" + exclude "**/R.java" + doLast { + copy { + from "src/main/javadoc" + into "$buildDir/docs/javadoc" + } + } + } +} diff --git a/library/all/build.gradle b/library/all/build.gradle new file mode 100644 index 0000000000..63943ada77 --- /dev/null +++ b/library/all/build.gradle @@ -0,0 +1,38 @@ +// 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. +apply plugin: 'com.android.library' + +android { + compileSdkVersion project.ext.compileSdkVersion + buildToolsVersion project.ext.buildToolsVersion + + defaultConfig { + minSdkVersion project.ext.minSdkVersion + targetSdkVersion project.ext.targetSdkVersion + } +} + +dependencies { + compile project(':library-core') + compile project(':library-dash') + compile project(':library-hls') + compile project(':library-smoothstreaming') + compile project(':library-ui') +} + +ext { + releaseArtifact = 'exoplayer' + releaseDescription = 'The ExoPlayer library (all modules).' +} +apply from: '../../publish.gradle' diff --git a/library/src/main/AndroidManifest.xml b/library/all/src/main/AndroidManifest.xml similarity index 100% rename from library/src/main/AndroidManifest.xml rename to library/all/src/main/AndroidManifest.xml diff --git a/library/build.gradle b/library/build.gradle deleted file mode 100644 index abca404cfa..0000000000 --- a/library/build.gradle +++ /dev/null @@ -1,87 +0,0 @@ -// 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. -import com.android.builder.core.BuilderConstants - -apply plugin: 'com.android.library' - -android { - compileSdkVersion project.ext.compileSdkVersion - buildToolsVersion project.ext.buildToolsVersion - - defaultConfig { - minSdkVersion project.ext.minSdkVersion - targetSdkVersion project.ext.targetSdkVersion - consumerProguardFiles 'proguard-rules.txt' - } - - buildTypes { - // Re-enable test coverage when the following issue is fixed: - // https://code.google.com/p/android/issues/detail?id=226070 - // debug { - // testCoverageEnabled = true - // } - } - - sourceSets { - androidTest { - java.srcDirs += "../testutils/src/main/java/" - } - } -} - -dependencies { - compile 'com.android.support:support-annotations:25.2.0' - androidTestCompile 'com.google.dexmaker:dexmaker:1.2' - androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2' - androidTestCompile 'org.mockito:mockito-core:1.9.5' -} - -android.libraryVariants.all { variant -> - def name = variant.buildType.name - if (name.equals(BuilderConstants.DEBUG)) { - return; // Skip debug builds. - } - def task = project.tasks.create "jar${name.capitalize()}", Jar - task.dependsOn variant.javaCompile - task.from variant.javaCompile.destinationDir - artifacts.add('archives', task); -} - -android.libraryVariants.all { variant -> - task("generate${variant.name.capitalize()}Javadoc", type: Javadoc) { - title = "ExoPlayer library" - description "Generates Javadoc for $variant.name." - source = variant.javaCompile.source - classpath = files(variant.javaCompile.classpath.files, project.android.getBootClasspath()) - options { - links "http://docs.oracle.com/javase/7/docs/api/" - linksOffline "https://developer.android.com/reference","${android.sdkDirectory}/docs/reference" - encoding = 'UTF-8' - } - exclude '**/BuildConfig.java' - exclude '**/R.java' - doLast { - copy { - from "src/main/javadoc" - into "$buildDir/docs/javadoc" - } - } - } -} - -ext { - releaseArtifact = 'exoplayer' - releaseDescription = 'The ExoPlayer library.' -} -apply from: '../publish.gradle' diff --git a/library/core/build.gradle b/library/core/build.gradle new file mode 100644 index 0000000000..bb0adaa4c7 --- /dev/null +++ b/library/core/build.gradle @@ -0,0 +1,54 @@ +// 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. +apply plugin: 'com.android.library' + +android { + compileSdkVersion project.ext.compileSdkVersion + buildToolsVersion project.ext.buildToolsVersion + + defaultConfig { + minSdkVersion project.ext.minSdkVersion + targetSdkVersion project.ext.targetSdkVersion + } + + sourceSets { + androidTest { + java.srcDirs += "../../testutils/src/main/java/" + } + } + + buildTypes { + debug { + testCoverageEnabled = true + } + } +} + +dependencies { + compile 'com.android.support:support-annotations:' + supportLibraryVersion + androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion + androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion + androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion +} + +ext { + javadocTitle = 'Core module' +} +apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'exoplayer-core' + releaseDescription = 'The ExoPlayer library core module.' +} +apply from: '../../publish.gradle' diff --git a/library/src/androidTest/AndroidManifest.xml b/library/core/src/androidTest/AndroidManifest.xml similarity index 90% rename from library/src/androidTest/AndroidManifest.xml rename to library/core/src/androidTest/AndroidManifest.xml index d8c7a2ce9f..9eab386b51 100644 --- a/library/src/androidTest/AndroidManifest.xml +++ b/library/core/src/androidTest/AndroidManifest.xml @@ -16,7 +16,7 @@ + package="com.google.android.exoplayer2.core.test"> @@ -27,7 +27,7 @@ diff --git a/library/src/androidTest/assets/flv/sample.flv b/library/core/src/androidTest/assets/flv/sample.flv similarity index 100% rename from library/src/androidTest/assets/flv/sample.flv rename to library/core/src/androidTest/assets/flv/sample.flv diff --git a/library/src/androidTest/assets/flv/sample.flv.0.dump b/library/core/src/androidTest/assets/flv/sample.flv.0.dump similarity index 100% rename from library/src/androidTest/assets/flv/sample.flv.0.dump rename to library/core/src/androidTest/assets/flv/sample.flv.0.dump diff --git a/library/src/androidTest/assets/mkv/sample.mkv b/library/core/src/androidTest/assets/mkv/sample.mkv similarity index 100% rename from library/src/androidTest/assets/mkv/sample.mkv rename to library/core/src/androidTest/assets/mkv/sample.mkv diff --git a/library/src/androidTest/assets/mkv/sample.mkv.0.dump b/library/core/src/androidTest/assets/mkv/sample.mkv.0.dump similarity index 100% rename from library/src/androidTest/assets/mkv/sample.mkv.0.dump rename to library/core/src/androidTest/assets/mkv/sample.mkv.0.dump diff --git a/library/src/androidTest/assets/mkv/sample.mkv.1.dump b/library/core/src/androidTest/assets/mkv/sample.mkv.1.dump similarity index 100% rename from library/src/androidTest/assets/mkv/sample.mkv.1.dump rename to library/core/src/androidTest/assets/mkv/sample.mkv.1.dump diff --git a/library/src/androidTest/assets/mkv/sample.mkv.2.dump b/library/core/src/androidTest/assets/mkv/sample.mkv.2.dump similarity index 100% rename from library/src/androidTest/assets/mkv/sample.mkv.2.dump rename to library/core/src/androidTest/assets/mkv/sample.mkv.2.dump diff --git a/library/src/androidTest/assets/mkv/sample.mkv.3.dump b/library/core/src/androidTest/assets/mkv/sample.mkv.3.dump similarity index 100% rename from library/src/androidTest/assets/mkv/sample.mkv.3.dump rename to library/core/src/androidTest/assets/mkv/sample.mkv.3.dump diff --git a/library/src/androidTest/assets/mkv/subsample_encrypted_altref.webm b/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm similarity index 100% rename from library/src/androidTest/assets/mkv/subsample_encrypted_altref.webm rename to library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm diff --git a/library/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump b/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump similarity index 100% rename from library/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump rename to library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump diff --git a/library/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm b/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm similarity index 100% rename from library/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm rename to library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm diff --git a/library/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump b/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump similarity index 100% rename from library/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump rename to library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump diff --git a/library/src/androidTest/assets/mp3/bear.mp3 b/library/core/src/androidTest/assets/mp3/bear.mp3 similarity index 100% rename from library/src/androidTest/assets/mp3/bear.mp3 rename to library/core/src/androidTest/assets/mp3/bear.mp3 diff --git a/library/src/androidTest/assets/mp3/bear.mp3.0.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.0.dump similarity index 100% rename from library/src/androidTest/assets/mp3/bear.mp3.0.dump rename to library/core/src/androidTest/assets/mp3/bear.mp3.0.dump diff --git a/library/src/androidTest/assets/mp3/bear.mp3.1.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump similarity index 100% rename from library/src/androidTest/assets/mp3/bear.mp3.1.dump rename to library/core/src/androidTest/assets/mp3/bear.mp3.1.dump diff --git a/library/src/androidTest/assets/mp3/bear.mp3.2.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump similarity index 100% rename from library/src/androidTest/assets/mp3/bear.mp3.2.dump rename to library/core/src/androidTest/assets/mp3/bear.mp3.2.dump diff --git a/library/src/androidTest/assets/mp3/bear.mp3.3.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.3.dump similarity index 100% rename from library/src/androidTest/assets/mp3/bear.mp3.3.dump rename to library/core/src/androidTest/assets/mp3/bear.mp3.3.dump diff --git a/library/src/androidTest/assets/mp3/play-trimmed.mp3 b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3 similarity index 100% rename from library/src/androidTest/assets/mp3/play-trimmed.mp3 rename to library/core/src/androidTest/assets/mp3/play-trimmed.mp3 diff --git a/library/src/androidTest/assets/mp3/play-trimmed.mp3.0.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.0.dump similarity index 100% rename from library/src/androidTest/assets/mp3/play-trimmed.mp3.0.dump rename to library/core/src/androidTest/assets/mp3/play-trimmed.mp3.0.dump diff --git a/library/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump similarity index 100% rename from library/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump rename to library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump diff --git a/library/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump similarity index 100% rename from library/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump rename to library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump diff --git a/library/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump similarity index 100% rename from library/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump rename to library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump diff --git a/library/src/androidTest/assets/mp3/play-trimmed.mp3.unklen.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.unklen.dump similarity index 100% rename from library/src/androidTest/assets/mp3/play-trimmed.mp3.unklen.dump rename to library/core/src/androidTest/assets/mp3/play-trimmed.mp3.unklen.dump diff --git a/library/src/androidTest/assets/mp4/sample.mp4 b/library/core/src/androidTest/assets/mp4/sample.mp4 similarity index 100% rename from library/src/androidTest/assets/mp4/sample.mp4 rename to library/core/src/androidTest/assets/mp4/sample.mp4 diff --git a/library/src/androidTest/assets/mp4/sample.mp4.0.dump b/library/core/src/androidTest/assets/mp4/sample.mp4.0.dump similarity index 100% rename from library/src/androidTest/assets/mp4/sample.mp4.0.dump rename to library/core/src/androidTest/assets/mp4/sample.mp4.0.dump diff --git a/library/src/androidTest/assets/mp4/sample.mp4.1.dump b/library/core/src/androidTest/assets/mp4/sample.mp4.1.dump similarity index 100% rename from library/src/androidTest/assets/mp4/sample.mp4.1.dump rename to library/core/src/androidTest/assets/mp4/sample.mp4.1.dump diff --git a/library/src/androidTest/assets/mp4/sample.mp4.2.dump b/library/core/src/androidTest/assets/mp4/sample.mp4.2.dump similarity index 100% rename from library/src/androidTest/assets/mp4/sample.mp4.2.dump rename to library/core/src/androidTest/assets/mp4/sample.mp4.2.dump diff --git a/library/src/androidTest/assets/mp4/sample.mp4.3.dump b/library/core/src/androidTest/assets/mp4/sample.mp4.3.dump similarity index 100% rename from library/src/androidTest/assets/mp4/sample.mp4.3.dump rename to library/core/src/androidTest/assets/mp4/sample.mp4.3.dump diff --git a/library/src/androidTest/assets/mp4/sample_fragmented.mp4 b/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4 similarity index 100% rename from library/src/androidTest/assets/mp4/sample_fragmented.mp4 rename to library/core/src/androidTest/assets/mp4/sample_fragmented.mp4 diff --git a/library/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump b/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump similarity index 100% rename from library/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump rename to library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump diff --git a/library/src/androidTest/assets/mp4/sample_fragmented_sei.mp4 b/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4 similarity index 100% rename from library/src/androidTest/assets/mp4/sample_fragmented_sei.mp4 rename to library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4 diff --git a/library/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump b/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump similarity index 100% rename from library/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump rename to library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump diff --git a/library/src/androidTest/assets/mp4/sample_fragmented_zero_size_atom.mp4 b/library/core/src/androidTest/assets/mp4/sample_fragmented_zero_size_atom.mp4 similarity index 100% rename from library/src/androidTest/assets/mp4/sample_fragmented_zero_size_atom.mp4 rename to library/core/src/androidTest/assets/mp4/sample_fragmented_zero_size_atom.mp4 diff --git a/library/src/androidTest/assets/ogg/bear.opus b/library/core/src/androidTest/assets/ogg/bear.opus similarity index 100% rename from library/src/androidTest/assets/ogg/bear.opus rename to library/core/src/androidTest/assets/ogg/bear.opus diff --git a/library/src/androidTest/assets/ogg/bear.opus.0.dump b/library/core/src/androidTest/assets/ogg/bear.opus.0.dump similarity index 100% rename from library/src/androidTest/assets/ogg/bear.opus.0.dump rename to library/core/src/androidTest/assets/ogg/bear.opus.0.dump diff --git a/library/src/androidTest/assets/ogg/bear.opus.1.dump b/library/core/src/androidTest/assets/ogg/bear.opus.1.dump similarity index 100% rename from library/src/androidTest/assets/ogg/bear.opus.1.dump rename to library/core/src/androidTest/assets/ogg/bear.opus.1.dump diff --git a/library/src/androidTest/assets/ogg/bear.opus.2.dump b/library/core/src/androidTest/assets/ogg/bear.opus.2.dump similarity index 100% rename from library/src/androidTest/assets/ogg/bear.opus.2.dump rename to library/core/src/androidTest/assets/ogg/bear.opus.2.dump diff --git a/library/src/androidTest/assets/ogg/bear.opus.3.dump b/library/core/src/androidTest/assets/ogg/bear.opus.3.dump similarity index 100% rename from library/src/androidTest/assets/ogg/bear.opus.3.dump rename to library/core/src/androidTest/assets/ogg/bear.opus.3.dump diff --git a/library/src/androidTest/assets/ogg/bear.opus.unklen.dump b/library/core/src/androidTest/assets/ogg/bear.opus.unklen.dump similarity index 100% rename from library/src/androidTest/assets/ogg/bear.opus.unklen.dump rename to library/core/src/androidTest/assets/ogg/bear.opus.unklen.dump diff --git a/library/src/androidTest/assets/ogg/bear_flac.ogg b/library/core/src/androidTest/assets/ogg/bear_flac.ogg similarity index 100% rename from library/src/androidTest/assets/ogg/bear_flac.ogg rename to library/core/src/androidTest/assets/ogg/bear_flac.ogg diff --git a/library/src/androidTest/assets/ogg/bear_flac.ogg.0.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump similarity index 100% rename from library/src/androidTest/assets/ogg/bear_flac.ogg.0.dump rename to library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump diff --git a/library/src/androidTest/assets/ogg/bear_flac.ogg.1.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump similarity index 100% rename from library/src/androidTest/assets/ogg/bear_flac.ogg.1.dump rename to library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump diff --git a/library/src/androidTest/assets/ogg/bear_flac.ogg.2.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump similarity index 100% rename from library/src/androidTest/assets/ogg/bear_flac.ogg.2.dump rename to library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump diff --git a/library/src/androidTest/assets/ogg/bear_flac.ogg.3.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump similarity index 100% rename from library/src/androidTest/assets/ogg/bear_flac.ogg.3.dump rename to library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump diff --git a/library/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump similarity index 100% rename from library/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump rename to library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump diff --git a/library/src/androidTest/assets/ogg/bear_flac_noseektable.ogg b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg similarity index 100% rename from library/src/androidTest/assets/ogg/bear_flac_noseektable.ogg rename to library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg diff --git a/library/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump similarity index 100% rename from library/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump rename to library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump diff --git a/library/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump similarity index 100% rename from library/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump rename to library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump diff --git a/library/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump similarity index 100% rename from library/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump rename to library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump diff --git a/library/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump similarity index 100% rename from library/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump rename to library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump diff --git a/library/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump similarity index 100% rename from library/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump rename to library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump diff --git a/library/src/androidTest/assets/ogg/bear_vorbis.ogg b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg similarity index 100% rename from library/src/androidTest/assets/ogg/bear_vorbis.ogg rename to library/core/src/androidTest/assets/ogg/bear_vorbis.ogg diff --git a/library/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump similarity index 100% rename from library/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump rename to library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump diff --git a/library/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump similarity index 100% rename from library/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump rename to library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump diff --git a/library/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump similarity index 100% rename from library/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump rename to library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump diff --git a/library/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump similarity index 100% rename from library/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump rename to library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump diff --git a/library/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump similarity index 100% rename from library/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump rename to library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump diff --git a/library/src/androidTest/assets/rawcc/sample.rawcc b/library/core/src/androidTest/assets/rawcc/sample.rawcc similarity index 100% rename from library/src/androidTest/assets/rawcc/sample.rawcc rename to library/core/src/androidTest/assets/rawcc/sample.rawcc diff --git a/library/src/androidTest/assets/rawcc/sample.rawcc.0.dump b/library/core/src/androidTest/assets/rawcc/sample.rawcc.0.dump similarity index 100% rename from library/src/androidTest/assets/rawcc/sample.rawcc.0.dump rename to library/core/src/androidTest/assets/rawcc/sample.rawcc.0.dump diff --git a/library/src/androidTest/assets/subrip/empty b/library/core/src/androidTest/assets/subrip/empty similarity index 100% rename from library/src/androidTest/assets/subrip/empty rename to library/core/src/androidTest/assets/subrip/empty diff --git a/library/src/androidTest/assets/subrip/no_end_timecodes b/library/core/src/androidTest/assets/subrip/no_end_timecodes similarity index 100% rename from library/src/androidTest/assets/subrip/no_end_timecodes rename to library/core/src/androidTest/assets/subrip/no_end_timecodes diff --git a/library/src/androidTest/assets/subrip/typical b/library/core/src/androidTest/assets/subrip/typical similarity index 100% rename from library/src/androidTest/assets/subrip/typical rename to library/core/src/androidTest/assets/subrip/typical diff --git a/library/src/androidTest/assets/subrip/typical_extra_blank_line b/library/core/src/androidTest/assets/subrip/typical_extra_blank_line similarity index 100% rename from library/src/androidTest/assets/subrip/typical_extra_blank_line rename to library/core/src/androidTest/assets/subrip/typical_extra_blank_line diff --git a/library/src/androidTest/assets/subrip/typical_missing_sequence b/library/core/src/androidTest/assets/subrip/typical_missing_sequence similarity index 100% rename from library/src/androidTest/assets/subrip/typical_missing_sequence rename to library/core/src/androidTest/assets/subrip/typical_missing_sequence diff --git a/library/src/androidTest/assets/subrip/typical_missing_timecode b/library/core/src/androidTest/assets/subrip/typical_missing_timecode similarity index 100% rename from library/src/androidTest/assets/subrip/typical_missing_timecode rename to library/core/src/androidTest/assets/subrip/typical_missing_timecode diff --git a/library/src/androidTest/assets/subrip/typical_negative_timestamps b/library/core/src/androidTest/assets/subrip/typical_negative_timestamps similarity index 100% rename from library/src/androidTest/assets/subrip/typical_negative_timestamps rename to library/core/src/androidTest/assets/subrip/typical_negative_timestamps diff --git a/library/src/androidTest/assets/subrip/typical_with_byte_order_mark b/library/core/src/androidTest/assets/subrip/typical_with_byte_order_mark similarity index 100% rename from library/src/androidTest/assets/subrip/typical_with_byte_order_mark rename to library/core/src/androidTest/assets/subrip/typical_with_byte_order_mark diff --git a/library/src/androidTest/assets/ts/sample.ac3 b/library/core/src/androidTest/assets/ts/sample.ac3 similarity index 100% rename from library/src/androidTest/assets/ts/sample.ac3 rename to library/core/src/androidTest/assets/ts/sample.ac3 diff --git a/library/src/androidTest/assets/ts/sample.ac3.0.dump b/library/core/src/androidTest/assets/ts/sample.ac3.0.dump similarity index 100% rename from library/src/androidTest/assets/ts/sample.ac3.0.dump rename to library/core/src/androidTest/assets/ts/sample.ac3.0.dump diff --git a/library/src/androidTest/assets/ts/sample.adts b/library/core/src/androidTest/assets/ts/sample.adts similarity index 100% rename from library/src/androidTest/assets/ts/sample.adts rename to library/core/src/androidTest/assets/ts/sample.adts diff --git a/library/src/androidTest/assets/ts/sample.adts.0.dump b/library/core/src/androidTest/assets/ts/sample.adts.0.dump similarity index 100% rename from library/src/androidTest/assets/ts/sample.adts.0.dump rename to library/core/src/androidTest/assets/ts/sample.adts.0.dump diff --git a/library/src/androidTest/assets/ts/sample.ps b/library/core/src/androidTest/assets/ts/sample.ps similarity index 100% rename from library/src/androidTest/assets/ts/sample.ps rename to library/core/src/androidTest/assets/ts/sample.ps diff --git a/library/src/androidTest/assets/ts/sample.ps.0.dump b/library/core/src/androidTest/assets/ts/sample.ps.0.dump similarity index 100% rename from library/src/androidTest/assets/ts/sample.ps.0.dump rename to library/core/src/androidTest/assets/ts/sample.ps.0.dump diff --git a/library/src/androidTest/assets/ts/sample.ts b/library/core/src/androidTest/assets/ts/sample.ts similarity index 100% rename from library/src/androidTest/assets/ts/sample.ts rename to library/core/src/androidTest/assets/ts/sample.ts diff --git a/library/src/androidTest/assets/ts/sample.ts.0.dump b/library/core/src/androidTest/assets/ts/sample.ts.0.dump similarity index 100% rename from library/src/androidTest/assets/ts/sample.ts.0.dump rename to library/core/src/androidTest/assets/ts/sample.ts.0.dump diff --git a/library/src/androidTest/assets/ts/sample_with_sdt.ts b/library/core/src/androidTest/assets/ts/sample_with_sdt.ts similarity index 100% rename from library/src/androidTest/assets/ts/sample_with_sdt.ts rename to library/core/src/androidTest/assets/ts/sample_with_sdt.ts diff --git a/library/src/androidTest/assets/ttml/chain_multiple_styles.xml b/library/core/src/androidTest/assets/ttml/chain_multiple_styles.xml similarity index 100% rename from library/src/androidTest/assets/ttml/chain_multiple_styles.xml rename to library/core/src/androidTest/assets/ttml/chain_multiple_styles.xml diff --git a/library/src/androidTest/assets/ttml/font_size.xml b/library/core/src/androidTest/assets/ttml/font_size.xml similarity index 100% rename from library/src/androidTest/assets/ttml/font_size.xml rename to library/core/src/androidTest/assets/ttml/font_size.xml diff --git a/library/src/androidTest/assets/ttml/font_size_empty.xml b/library/core/src/androidTest/assets/ttml/font_size_empty.xml similarity index 100% rename from library/src/androidTest/assets/ttml/font_size_empty.xml rename to library/core/src/androidTest/assets/ttml/font_size_empty.xml diff --git a/library/src/androidTest/assets/ttml/font_size_invalid.xml b/library/core/src/androidTest/assets/ttml/font_size_invalid.xml similarity index 100% rename from library/src/androidTest/assets/ttml/font_size_invalid.xml rename to library/core/src/androidTest/assets/ttml/font_size_invalid.xml diff --git a/library/src/androidTest/assets/ttml/font_size_no_unit.xml b/library/core/src/androidTest/assets/ttml/font_size_no_unit.xml similarity index 100% rename from library/src/androidTest/assets/ttml/font_size_no_unit.xml rename to library/core/src/androidTest/assets/ttml/font_size_no_unit.xml diff --git a/library/src/androidTest/assets/ttml/frame_rate.xml b/library/core/src/androidTest/assets/ttml/frame_rate.xml similarity index 100% rename from library/src/androidTest/assets/ttml/frame_rate.xml rename to library/core/src/androidTest/assets/ttml/frame_rate.xml diff --git a/library/src/androidTest/assets/ttml/inherit_and_override_style.xml b/library/core/src/androidTest/assets/ttml/inherit_and_override_style.xml similarity index 100% rename from library/src/androidTest/assets/ttml/inherit_and_override_style.xml rename to library/core/src/androidTest/assets/ttml/inherit_and_override_style.xml diff --git a/library/src/androidTest/assets/ttml/inherit_global_and_parent.xml b/library/core/src/androidTest/assets/ttml/inherit_global_and_parent.xml similarity index 100% rename from library/src/androidTest/assets/ttml/inherit_global_and_parent.xml rename to library/core/src/androidTest/assets/ttml/inherit_global_and_parent.xml diff --git a/library/src/androidTest/assets/ttml/inherit_multiple_styles.xml b/library/core/src/androidTest/assets/ttml/inherit_multiple_styles.xml similarity index 100% rename from library/src/androidTest/assets/ttml/inherit_multiple_styles.xml rename to library/core/src/androidTest/assets/ttml/inherit_multiple_styles.xml diff --git a/library/src/androidTest/assets/ttml/inherit_style.xml b/library/core/src/androidTest/assets/ttml/inherit_style.xml similarity index 100% rename from library/src/androidTest/assets/ttml/inherit_style.xml rename to library/core/src/androidTest/assets/ttml/inherit_style.xml diff --git a/library/src/androidTest/assets/ttml/inline_style_attributes.xml b/library/core/src/androidTest/assets/ttml/inline_style_attributes.xml similarity index 100% rename from library/src/androidTest/assets/ttml/inline_style_attributes.xml rename to library/core/src/androidTest/assets/ttml/inline_style_attributes.xml diff --git a/library/src/androidTest/assets/ttml/multiple_regions.xml b/library/core/src/androidTest/assets/ttml/multiple_regions.xml similarity index 100% rename from library/src/androidTest/assets/ttml/multiple_regions.xml rename to library/core/src/androidTest/assets/ttml/multiple_regions.xml diff --git a/library/src/androidTest/assets/ttml/no_underline_linethrough.xml b/library/core/src/androidTest/assets/ttml/no_underline_linethrough.xml similarity index 100% rename from library/src/androidTest/assets/ttml/no_underline_linethrough.xml rename to library/core/src/androidTest/assets/ttml/no_underline_linethrough.xml diff --git a/library/core/src/androidTest/assets/tx3g/initialization b/library/core/src/androidTest/assets/tx3g/initialization new file mode 100644 index 0000000000..def42b9ade Binary files /dev/null and b/library/core/src/androidTest/assets/tx3g/initialization differ diff --git a/library/core/src/androidTest/assets/tx3g/initialization_all_defaults b/library/core/src/androidTest/assets/tx3g/initialization_all_defaults new file mode 100644 index 0000000000..be2f92b5f2 Binary files /dev/null and b/library/core/src/androidTest/assets/tx3g/initialization_all_defaults differ diff --git a/library/core/src/androidTest/assets/tx3g/no_subtitle b/library/core/src/androidTest/assets/tx3g/no_subtitle new file mode 100644 index 0000000000..09f370e38f Binary files /dev/null and b/library/core/src/androidTest/assets/tx3g/no_subtitle differ diff --git a/library/core/src/androidTest/assets/tx3g/sample_just_text b/library/core/src/androidTest/assets/tx3g/sample_just_text new file mode 100644 index 0000000000..68561eca7e Binary files /dev/null and b/library/core/src/androidTest/assets/tx3g/sample_just_text differ diff --git a/library/core/src/androidTest/assets/tx3g/sample_utf16_be_no_styl b/library/core/src/androidTest/assets/tx3g/sample_utf16_be_no_styl new file mode 100644 index 0000000000..9c3fed2b7d Binary files /dev/null and b/library/core/src/androidTest/assets/tx3g/sample_utf16_be_no_styl differ diff --git a/library/core/src/androidTest/assets/tx3g/sample_utf16_le_no_styl b/library/core/src/androidTest/assets/tx3g/sample_utf16_le_no_styl new file mode 100644 index 0000000000..03a2cb2350 Binary files /dev/null and b/library/core/src/androidTest/assets/tx3g/sample_utf16_le_no_styl differ diff --git a/library/core/src/androidTest/assets/tx3g/sample_with_multiple_styl b/library/core/src/androidTest/assets/tx3g/sample_with_multiple_styl new file mode 100644 index 0000000000..48b01c3438 Binary files /dev/null and b/library/core/src/androidTest/assets/tx3g/sample_with_multiple_styl differ diff --git a/library/core/src/androidTest/assets/tx3g/sample_with_other_extension b/library/core/src/androidTest/assets/tx3g/sample_with_other_extension new file mode 100644 index 0000000000..e2eb7ac002 Binary files /dev/null and b/library/core/src/androidTest/assets/tx3g/sample_with_other_extension differ diff --git a/library/core/src/androidTest/assets/tx3g/sample_with_styl b/library/core/src/androidTest/assets/tx3g/sample_with_styl new file mode 100644 index 0000000000..b0f0d54265 Binary files /dev/null and b/library/core/src/androidTest/assets/tx3g/sample_with_styl differ diff --git a/library/core/src/androidTest/assets/tx3g/sample_with_styl_all_defaults b/library/core/src/androidTest/assets/tx3g/sample_with_styl_all_defaults new file mode 100644 index 0000000000..c1dec5d3c9 Binary files /dev/null and b/library/core/src/androidTest/assets/tx3g/sample_with_styl_all_defaults differ diff --git a/library/core/src/androidTest/assets/tx3g/sample_with_tbox b/library/core/src/androidTest/assets/tx3g/sample_with_tbox new file mode 100644 index 0000000000..72a8fb97cb Binary files /dev/null and b/library/core/src/androidTest/assets/tx3g/sample_with_tbox differ diff --git a/library/src/androidTest/assets/wav/sample.wav b/library/core/src/androidTest/assets/wav/sample.wav similarity index 100% rename from library/src/androidTest/assets/wav/sample.wav rename to library/core/src/androidTest/assets/wav/sample.wav diff --git a/library/src/androidTest/assets/wav/sample.wav.0.dump b/library/core/src/androidTest/assets/wav/sample.wav.0.dump similarity index 100% rename from library/src/androidTest/assets/wav/sample.wav.0.dump rename to library/core/src/androidTest/assets/wav/sample.wav.0.dump diff --git a/library/src/androidTest/assets/wav/sample.wav.1.dump b/library/core/src/androidTest/assets/wav/sample.wav.1.dump similarity index 100% rename from library/src/androidTest/assets/wav/sample.wav.1.dump rename to library/core/src/androidTest/assets/wav/sample.wav.1.dump diff --git a/library/src/androidTest/assets/wav/sample.wav.2.dump b/library/core/src/androidTest/assets/wav/sample.wav.2.dump similarity index 100% rename from library/src/androidTest/assets/wav/sample.wav.2.dump rename to library/core/src/androidTest/assets/wav/sample.wav.2.dump diff --git a/library/src/androidTest/assets/wav/sample.wav.3.dump b/library/core/src/androidTest/assets/wav/sample.wav.3.dump similarity index 100% rename from library/src/androidTest/assets/wav/sample.wav.3.dump rename to library/core/src/androidTest/assets/wav/sample.wav.3.dump diff --git a/library/src/androidTest/assets/webm/vorbis_codec_private b/library/core/src/androidTest/assets/webm/vorbis_codec_private similarity index 100% rename from library/src/androidTest/assets/webm/vorbis_codec_private rename to library/core/src/androidTest/assets/webm/vorbis_codec_private diff --git a/library/src/androidTest/assets/webvtt/empty b/library/core/src/androidTest/assets/webvtt/empty similarity index 100% rename from library/src/androidTest/assets/webvtt/empty rename to library/core/src/androidTest/assets/webvtt/empty diff --git a/library/src/androidTest/assets/webvtt/typical b/library/core/src/androidTest/assets/webvtt/typical similarity index 100% rename from library/src/androidTest/assets/webvtt/typical rename to library/core/src/androidTest/assets/webvtt/typical diff --git a/library/src/androidTest/assets/webvtt/typical_with_comments b/library/core/src/androidTest/assets/webvtt/typical_with_comments similarity index 100% rename from library/src/androidTest/assets/webvtt/typical_with_comments rename to library/core/src/androidTest/assets/webvtt/typical_with_comments diff --git a/library/src/androidTest/assets/webvtt/typical_with_identifiers b/library/core/src/androidTest/assets/webvtt/typical_with_identifiers similarity index 100% rename from library/src/androidTest/assets/webvtt/typical_with_identifiers rename to library/core/src/androidTest/assets/webvtt/typical_with_identifiers diff --git a/library/src/androidTest/assets/webvtt/with_bad_cue_header b/library/core/src/androidTest/assets/webvtt/with_bad_cue_header similarity index 100% rename from library/src/androidTest/assets/webvtt/with_bad_cue_header rename to library/core/src/androidTest/assets/webvtt/with_bad_cue_header diff --git a/library/src/androidTest/assets/webvtt/with_css_complex_selectors b/library/core/src/androidTest/assets/webvtt/with_css_complex_selectors similarity index 100% rename from library/src/androidTest/assets/webvtt/with_css_complex_selectors rename to library/core/src/androidTest/assets/webvtt/with_css_complex_selectors diff --git a/library/src/androidTest/assets/webvtt/with_css_styles b/library/core/src/androidTest/assets/webvtt/with_css_styles similarity index 100% rename from library/src/androidTest/assets/webvtt/with_css_styles rename to library/core/src/androidTest/assets/webvtt/with_css_styles diff --git a/library/src/androidTest/assets/webvtt/with_positioning b/library/core/src/androidTest/assets/webvtt/with_positioning similarity index 100% rename from library/src/androidTest/assets/webvtt/with_positioning rename to library/core/src/androidTest/assets/webvtt/with_positioning diff --git a/library/src/androidTest/assets/webvtt/with_tags b/library/core/src/androidTest/assets/webvtt/with_tags similarity index 100% rename from library/src/androidTest/assets/webvtt/with_tags rename to library/core/src/androidTest/assets/webvtt/with_tags diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/CTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/CTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/CTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/CTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java similarity index 80% rename from library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 93c0a7dc11..2c10bfe6a0 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2; import android.os.Handler; import android.os.HandlerThread; +import android.util.Pair; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; @@ -32,6 +33,7 @@ import com.google.android.exoplayer2.util.MediaClock; import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; import java.util.ArrayList; +import java.util.LinkedList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -70,8 +72,7 @@ public final class ExoPlayerTest extends TestCase { assertEquals(0, renderer.formatReadCount); assertEquals(0, renderer.bufferReadCount); assertFalse(renderer.isEnded); - assertEquals(timeline, playerWrapper.timeline); - assertNull(playerWrapper.manifest); + playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null)); } /** @@ -89,9 +90,8 @@ public final class ExoPlayerTest extends TestCase { assertEquals(1, renderer.formatReadCount); assertEquals(1, renderer.bufferReadCount); assertTrue(renderer.isEnded); - assertEquals(timeline, playerWrapper.timeline); - assertEquals(manifest, playerWrapper.manifest); assertEquals(new TrackGroupArray(new TrackGroup(TEST_VIDEO_FORMAT)), playerWrapper.trackGroups); + playerWrapper.assertSourceInfosEquals(Pair.create(timeline, manifest)); } /** @@ -111,8 +111,7 @@ public final class ExoPlayerTest extends TestCase { assertEquals(3, renderer.formatReadCount); assertEquals(1, renderer.bufferReadCount); assertTrue(renderer.isEnded); - assertEquals(timeline, playerWrapper.timeline); - assertNull(playerWrapper.manifest); + playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null)); } /** @@ -140,6 +139,16 @@ public final class ExoPlayerTest extends TestCase { return isCurrentStreamFinal() ? 60000030 : 60000000; } + @Override + public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { + return PlaybackParameters.DEFAULT; + } + + @Override + public PlaybackParameters getPlaybackParameters() { + return PlaybackParameters.DEFAULT; + } + @Override public boolean isEnded() { // Allow playback to end once the final period is playing. @@ -153,8 +162,60 @@ public final class ExoPlayerTest extends TestCase { assertEquals(1, audioRenderer.positionResetCount); assertTrue(videoRenderer.isEnded); assertTrue(audioRenderer.isEnded); - assertEquals(timeline, playerWrapper.timeline); - assertNull(playerWrapper.manifest); + playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null)); + } + + public void testRepreparationGivesFreshSourceInfo() throws Exception { + PlayerWrapper playerWrapper = new PlayerWrapper(); + Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0)); + FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT); + + // Prepare the player with a source with the first manifest and a non-empty timeline + Object firstSourceManifest = new Object(); + playerWrapper.setup(new FakeMediaSource(timeline, firstSourceManifest, TEST_VIDEO_FORMAT), + renderer); + playerWrapper.blockUntilSourceInfoRefreshed(TIMEOUT_MS); + + // Prepare the player again with a source and a new manifest, which will never be exposed. + final CountDownLatch queuedSourceInfoCountDownLatch = new CountDownLatch(1); + final CountDownLatch completePreparationCountDownLatch = new CountDownLatch(1); + playerWrapper.prepare(new FakeMediaSource(timeline, new Object(), TEST_VIDEO_FORMAT) { + @Override + public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + super.prepareSource(player, isTopLevelSource, listener); + // We've queued a source info refresh on the playback thread's event queue. Allow the test + // thread to prepare the player with the third source, and block this thread (the playback + // thread) until the test thread's call to prepare() has returned. + queuedSourceInfoCountDownLatch.countDown(); + try { + completePreparationCountDownLatch.await(); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + } + }); + + // Prepare the player again with a third source. + queuedSourceInfoCountDownLatch.await(); + Object thirdSourceManifest = new Object(); + playerWrapper.prepare(new FakeMediaSource(timeline, thirdSourceManifest, TEST_VIDEO_FORMAT)); + completePreparationCountDownLatch.countDown(); + + // Wait for playback to complete. + playerWrapper.blockUntilEnded(TIMEOUT_MS); + assertEquals(0, playerWrapper.positionDiscontinuityCount); + assertEquals(1, renderer.formatReadCount); + assertEquals(1, renderer.bufferReadCount); + assertTrue(renderer.isEnded); + assertEquals(new TrackGroupArray(new TrackGroup(TEST_VIDEO_FORMAT)), playerWrapper.trackGroups); + + // The first source's preparation completed with a non-empty timeline. When the player was + // re-prepared with the second source, it immediately exposed an empty timeline, but the source + // info refresh from the second source was suppressed as we re-prepared with the third source. + playerWrapper.assertSourceInfosEquals( + Pair.create(timeline, firstSourceManifest), + Pair.create(Timeline.EMPTY, null), + Pair.create(timeline, thirdSourceManifest)); } /** @@ -162,13 +223,13 @@ public final class ExoPlayerTest extends TestCase { */ private static final class PlayerWrapper implements ExoPlayer.EventListener { + private final CountDownLatch sourceInfoCountDownLatch; private final CountDownLatch endedCountDownLatch; private final HandlerThread playerThread; private final Handler handler; + private final LinkedList> sourceInfos; private ExoPlayer player; - private Timeline timeline; - private Object manifest; private TrackGroupArray trackGroups; private Exception exception; @@ -176,17 +237,19 @@ public final class ExoPlayerTest extends TestCase { private volatile int positionDiscontinuityCount; public PlayerWrapper() { + sourceInfoCountDownLatch = new CountDownLatch(1); endedCountDownLatch = new CountDownLatch(1); playerThread = new HandlerThread("ExoPlayerTest thread"); playerThread.start(); handler = new Handler(playerThread.getLooper()); + sourceInfos = new LinkedList<>(); } // Called on the test thread. public void blockUntilEnded(long timeoutMs) throws Exception { if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { - exception = new TimeoutException("Test playback timed out."); + exception = new TimeoutException("Test playback timed out waiting for playback to end."); } release(); // Throw any pending exception (from playback, timing out or releasing). @@ -195,6 +258,12 @@ public final class ExoPlayerTest extends TestCase { } } + public void blockUntilSourceInfoRefreshed(long timeoutMs) throws Exception { + if (!sourceInfoCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("Test playback timed out waiting for source info."); + } + } + public void setup(final MediaSource mediaSource, final Renderer... renderers) { handler.post(new Runnable() { @Override @@ -211,6 +280,19 @@ public final class ExoPlayerTest extends TestCase { }); } + public void prepare(final MediaSource mediaSource) { + handler.post(new Runnable() { + @Override + public void run() { + try { + player.prepare(mediaSource); + } catch (Exception e) { + handleError(e); + } + } + }); + } + public void release() throws InterruptedException { handler.post(new Runnable() { @Override @@ -236,6 +318,14 @@ public final class ExoPlayerTest extends TestCase { endedCountDownLatch.countDown(); } + @SafeVarargs + public final void assertSourceInfosEquals(Pair... sourceInfos) { + assertEquals(sourceInfos.length, this.sourceInfos.size()); + for (Pair sourceInfo : sourceInfos) { + assertEquals(sourceInfo, this.sourceInfos.remove()); + } + } + // ExoPlayer.EventListener implementation. @Override @@ -252,8 +342,8 @@ public final class ExoPlayerTest extends TestCase { @Override public void onTimelineChanged(Timeline timeline, Object manifest) { - this.timeline = timeline; - this.manifest = manifest; + sourceInfos.add(Pair.create(timeline, manifest)); + sourceInfoCountDownLatch.countDown(); } @Override @@ -272,6 +362,11 @@ public final class ExoPlayerTest extends TestCase { positionDiscontinuityCount++; } + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + // Do nothing. + } + } private static final class TimelineWindowDefinition { @@ -319,7 +414,7 @@ public final class ExoPlayerTest extends TestCase { public Period getPeriod(int periodIndex, Period period, boolean setIds) { TimelineWindowDefinition windowDefinition = windowDefinitions[periodIndex]; Object id = setIds ? periodIndex : null; - return period.set(id, id, periodIndex, windowDefinition.durationUs, 0); + return period.set(id, id, periodIndex, windowDefinition.durationUs, 0, false); } @Override @@ -337,7 +432,7 @@ public final class ExoPlayerTest extends TestCase { * Fake {@link MediaSource} that provides a given timeline (which must have one period). Creating * the period will return a {@link FakeMediaPeriod}. */ - private static final class FakeMediaSource implements MediaSource { + private static class FakeMediaSource implements MediaSource { private final Timeline timeline; private final Object manifest; @@ -536,7 +631,7 @@ public final class ExoPlayerTest extends TestCase { } @Override - public void skipToKeyframeBefore(long timeUs) { + public void skipData(long positionUs) { // Do nothing. } diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java similarity index 93% rename from library/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java index e13afceb40..a47a3fb12d 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java @@ -29,6 +29,7 @@ import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.video.ColorInfo; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; @@ -61,11 +62,13 @@ public final class FormatTest extends TestCase { Metadata metadata = new Metadata( new TextInformationFrame("id1", "description1", "value1"), new TextInformationFrame("id2", "description2", "value2")); + ColorInfo colorInfo = new ColorInfo(C.COLOR_SPACE_BT709, + C.COLOR_RANGE_LIMITED, C.COLOR_TRANSFER_SDR, new byte[] {1, 2, 3, 4, 5, 6, 7}); Format formatToParcel = new Format("id", MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null, - 1024, 2048, 1920, 1080, 24, 90, 2, projectionData, C.STEREO_MODE_TOP_BOTTOM, 6, 44100, - C.ENCODING_PCM_24BIT, 1001, 1002, 0, "und", Format.NO_VALUE, Format.OFFSET_SAMPLE_RELATIVE, - INIT_DATA, drmInitData, metadata); + 1024, 2048, 1920, 1080, 24, 90, 2, projectionData, C.STEREO_MODE_TOP_BOTTOM, colorInfo, 6, + 44100, C.ENCODING_PCM_24BIT, 1001, 1002, 0, "und", Format.NO_VALUE, + Format.OFFSET_SAMPLE_RELATIVE, INIT_DATA, drmInitData, metadata); Parcel parcel = Parcel.obtain(); formatToParcel.writeToParcel(parcel, 0); diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java similarity index 56% rename from library/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java index 985e93404a..afd690762b 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java @@ -23,17 +23,9 @@ import android.test.InstrumentationTestCase; import android.test.MoreAsserts; import android.util.Pair; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; -import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; -import com.google.android.exoplayer2.source.dash.manifest.DashManifest; -import com.google.android.exoplayer2.source.dash.manifest.Period; -import com.google.android.exoplayer2.source.dash.manifest.Representation; -import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.HttpDataSource; -import com.google.android.exoplayer2.util.MimeTypes; -import java.util.Arrays; import java.util.HashMap; import org.mockito.Mock; @@ -50,89 +42,57 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { TestUtil.setUpMockito(this); - when(mediaDrm.openSession()).thenReturn(new byte[] {1, 2, 3}); - offlineLicenseHelper = new OfflineLicenseHelper<>(mediaDrm, mediaDrmCallback, null); } @Override protected void tearDown() throws Exception { - offlineLicenseHelper.releaseResources(); + offlineLicenseHelper.release(); + offlineLicenseHelper = null; } public void testDownloadRenewReleaseKey() throws Exception { - DashManifest manifest = newDashManifestWithAllElements(); setStubLicenseAndPlaybackDurationValues(1000, 200); byte[] keySetId = {2, 5, 8}; setStubKeySetId(keySetId); - byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest); + byte[] offlineLicenseKeySetId = offlineLicenseHelper.downloadLicense(newDrmInitData()); assertOfflineLicenseKeySetIdEqual(keySetId, offlineLicenseKeySetId); byte[] keySetId2 = {6, 7, 0, 1, 4}; setStubKeySetId(keySetId2); - byte[] offlineLicenseKeySetId2 = offlineLicenseHelper.renew(offlineLicenseKeySetId); + byte[] offlineLicenseKeySetId2 = offlineLicenseHelper.renewLicense(offlineLicenseKeySetId); assertOfflineLicenseKeySetIdEqual(keySetId2, offlineLicenseKeySetId2); - offlineLicenseHelper.release(offlineLicenseKeySetId2); + offlineLicenseHelper.releaseLicense(offlineLicenseKeySetId2); } - public void testDownloadFailsIfThereIsNoInitData() throws Exception { - setDefaultStubValues(); - DashManifest manifest = - newDashManifest(newPeriods(newAdaptationSets(newRepresentations(null /*no init data*/)))); - - byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest); - - assertNull(offlineLicenseKeySetId); + public void testDownloadLicenseFailsIfNullInitData() throws Exception { + try { + offlineLicenseHelper.downloadLicense(null); + fail(); + } catch (IllegalArgumentException e) { + // Expected. + } } - public void testDownloadFailsIfThereIsNoRepresentation() throws Exception { - setDefaultStubValues(); - DashManifest manifest = newDashManifest(newPeriods(newAdaptationSets(/*no representation*/))); - - byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest); - - assertNull(offlineLicenseKeySetId); - } - - public void testDownloadFailsIfThereIsNoAdaptationSet() throws Exception { - setDefaultStubValues(); - DashManifest manifest = newDashManifest(newPeriods(/*no adaptation set*/)); - - byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest); - - assertNull(offlineLicenseKeySetId); - } - - public void testDownloadFailsIfThereIsNoPeriod() throws Exception { - setDefaultStubValues(); - DashManifest manifest = newDashManifest(/*no periods*/); - - byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest); - - assertNull(offlineLicenseKeySetId); - } - - public void testDownloadFailsIfNoKeySetIdIsReturned() throws Exception { + public void testDownloadLicenseFailsIfNoKeySetIdIsReturned() throws Exception { setStubLicenseAndPlaybackDurationValues(1000, 200); - DashManifest manifest = newDashManifestWithAllElements(); - byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest); + byte[] offlineLicenseKeySetId = offlineLicenseHelper.downloadLicense(newDrmInitData()); assertNull(offlineLicenseKeySetId); } - public void testDownloadDoesNotFailIfDurationNotAvailable() throws Exception { + public void testDownloadLicenseDoesNotFailIfDurationNotAvailable() throws Exception { setDefaultStubKeySetId(); - DashManifest manifest = newDashManifestWithAllElements(); - byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest); + byte[] offlineLicenseKeySetId = offlineLicenseHelper.downloadLicense(newDrmInitData()); assertNotNull(offlineLicenseKeySetId); } @@ -142,9 +102,8 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { int playbackDuration = 200; setStubLicenseAndPlaybackDurationValues(licenseDuration, playbackDuration); setDefaultStubKeySetId(); - DashManifest manifest = newDashManifestWithAllElements(); - byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest); + byte[] offlineLicenseKeySetId = offlineLicenseHelper.downloadLicense(newDrmInitData()); Pair licenseDurationRemainingSec = offlineLicenseHelper .getLicenseDurationRemainingSec(offlineLicenseKeySetId); @@ -158,9 +117,8 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { int playbackDuration = 0; setStubLicenseAndPlaybackDurationValues(licenseDuration, playbackDuration); setDefaultStubKeySetId(); - DashManifest manifest = newDashManifestWithAllElements(); - byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest); + byte[] offlineLicenseKeySetId = offlineLicenseHelper.downloadLicense(newDrmInitData()); Pair licenseDurationRemainingSec = offlineLicenseHelper .getLicenseDurationRemainingSec(offlineLicenseKeySetId); @@ -169,12 +127,6 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { assertEquals(playbackDuration, (long) licenseDurationRemainingSec.second); } - private void setDefaultStubValues() - throws android.media.NotProvisionedException, android.media.DeniedByServerException { - setDefaultStubKeySetId(); - setStubLicenseAndPlaybackDurationValues(1000, 200); - } - private void setDefaultStubKeySetId() throws android.media.NotProvisionedException, android.media.DeniedByServerException { setStubKeySetId(new byte[] {2, 5, 8}); @@ -201,34 +153,9 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { when(mediaDrm.queryKeyStatus(any(byte[].class))).thenReturn(keyStatus); } - private static DashManifest newDashManifestWithAllElements() { - return newDashManifest(newPeriods(newAdaptationSets(newRepresentations(newDrmInitData())))); - } - - private static DashManifest newDashManifest(Period... periods) { - return new DashManifest(0, 0, 0, false, 0, 0, 0, null, null, Arrays.asList(periods)); - } - - private static Period newPeriods(AdaptationSet... adaptationSets) { - return new Period("", 0, Arrays.asList(adaptationSets)); - } - - private static AdaptationSet newAdaptationSets(Representation... representations) { - return new AdaptationSet(0, C.TRACK_TYPE_VIDEO, Arrays.asList(representations), null); - } - - private static Representation newRepresentations(DrmInitData drmInitData) { - Format format = Format.createVideoContainerFormat("id", MimeTypes.VIDEO_MP4, - MimeTypes.VIDEO_H264, "", Format.NO_VALUE, 1024, 768, Format.NO_VALUE, null, 0); - if (drmInitData != null) { - format = format.copyWithDrmInitData(drmInitData); - } - return Representation.newInstance("", 0, format, "", new SingleSegmentBase()); - } - private static DrmInitData newDrmInitData() { return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "mimeType", - new byte[]{1, 4, 7, 0, 3, 6})); + new byte[] {1, 4, 7, 0, 3, 6})); } } diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java similarity index 93% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java index dcab0c6275..6abd116086 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java @@ -269,9 +269,8 @@ public class DefaultExtractorInputTest extends TestCase { public void testSkipFullyLarge() throws Exception { // Tests skipping an amount of data that's larger than any internal scratch space. int largeSkipSize = 1024 * 1024; - FakeDataSource.Builder builder = new FakeDataSource.Builder(); - builder.appendReadData(new byte[largeSkipSize]); - FakeDataSource testDataSource = builder.build(); + FakeDataSource testDataSource = new FakeDataSource(); + testDataSource.getDataSet().newDefaultData().appendReadData(new byte[largeSkipSize]); testDataSource.open(new DataSpec(Uri.parse(TEST_URI))); DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET); @@ -397,29 +396,29 @@ public class DefaultExtractorInputTest extends TestCase { } private static FakeDataSource buildDataSource() throws Exception { - FakeDataSource.Builder builder = new FakeDataSource.Builder(); - builder.appendReadData(Arrays.copyOfRange(TEST_DATA, 0, 3)); - builder.appendReadData(Arrays.copyOfRange(TEST_DATA, 3, 6)); - builder.appendReadData(Arrays.copyOfRange(TEST_DATA, 6, 9)); - FakeDataSource testDataSource = builder.build(); + FakeDataSource testDataSource = new FakeDataSource(); + testDataSource.getDataSet().newDefaultData() + .appendReadData(Arrays.copyOfRange(TEST_DATA, 0, 3)) + .appendReadData(Arrays.copyOfRange(TEST_DATA, 3, 6)) + .appendReadData(Arrays.copyOfRange(TEST_DATA, 6, 9)); testDataSource.open(new DataSpec(Uri.parse(TEST_URI))); return testDataSource; } private static FakeDataSource buildFailingDataSource() throws Exception { - FakeDataSource.Builder builder = new FakeDataSource.Builder(); - builder.appendReadData(Arrays.copyOfRange(TEST_DATA, 0, 6)); - builder.appendReadError(new IOException()); - builder.appendReadData(Arrays.copyOfRange(TEST_DATA, 6, 9)); - FakeDataSource testDataSource = builder.build(); + FakeDataSource testDataSource = new FakeDataSource(); + testDataSource.getDataSet().newDefaultData() + .appendReadData(Arrays.copyOfRange(TEST_DATA, 0, 6)) + .appendReadError(new IOException()) + .appendReadData(Arrays.copyOfRange(TEST_DATA, 6, 9)); testDataSource.open(new DataSpec(Uri.parse(TEST_URI))); return testDataSource; } private static FakeDataSource buildLargeDataSource() throws Exception { - FakeDataSource.Builder builder = new FakeDataSource.Builder(); - builder.appendReadData(new byte[LARGE_TEST_DATA_LENGTH]); - FakeDataSource testDataSource = builder.build(); + FakeDataSource testDataSource = new FakeDataSource(); + testDataSource.getDataSet().newDefaultData() + .appendReadData(new byte[LARGE_TEST_DATA_LENGTH]); testDataSource.open(new DataSpec(Uri.parse(TEST_URI))); return testDataSource; } diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ExtractorTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/ExtractorTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ExtractorTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java similarity index 99% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java index 2d32839c51..d0213337b8 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.extractor.mp4; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; - import junit.framework.TestCase; /** diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/TestData.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/TestData.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/TestData.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/TestData.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArrayTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArrayTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArrayTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArrayTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtilTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtilTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtilTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtilTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java new file mode 100644 index 0000000000..6b39ed1645 --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java @@ -0,0 +1,221 @@ +/* + * 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.google.android.exoplayer2.metadata.id3; + +import android.test.MoreAsserts; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.MetadataDecoderException; +import com.google.android.exoplayer2.util.Assertions; +import junit.framework.TestCase; + +/** + * Test for {@link Id3Decoder}. + */ +public final class Id3DecoderTest extends TestCase { + + private static final byte[] TAG_HEADER = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 0}; + private static final int FRAME_HEADER_LENGTH = 10; + private static final int ID3_TEXT_ENCODING_UTF_8 = 3; + + public void testDecodeTxxxFrame() throws MetadataDecoderException { + byte[] rawId3 = buildSingleFrameTag("TXXX", new byte[] {3, 0, 109, 100, 105, 97, 108, 111, 103, + 95, 86, 73, 78, 68, 73, 67, 79, 49, 53, 50, 55, 54, 54, 52, 95, 115, 116, 97, 114, 116, 0}); + Id3Decoder decoder = new Id3Decoder(); + Metadata metadata = decoder.decode(rawId3, rawId3.length); + assertEquals(1, metadata.length()); + TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); + assertEquals("TXXX", textInformationFrame.id); + assertEquals("", textInformationFrame.description); + assertEquals("mdialog_VINDICO1527664_start", textInformationFrame.value); + + // Test empty. + rawId3 = buildSingleFrameTag("TXXX", new byte[0]); + metadata = decoder.decode(rawId3, rawId3.length); + assertEquals(0, metadata.length()); + + // Test encoding byte only. + rawId3 = buildSingleFrameTag("TXXX", new byte[] {ID3_TEXT_ENCODING_UTF_8}); + metadata = decoder.decode(rawId3, rawId3.length); + assertEquals(1, metadata.length()); + textInformationFrame = (TextInformationFrame) metadata.get(0); + assertEquals("TXXX", textInformationFrame.id); + assertEquals("", textInformationFrame.description); + assertEquals("", textInformationFrame.value); + } + + public void testDecodeTextInformationFrame() throws MetadataDecoderException { + byte[] rawId3 = buildSingleFrameTag("TIT2", new byte[] {3, 72, 101, 108, 108, 111, 32, 87, 111, + 114, 108, 100, 0}); + Id3Decoder decoder = new Id3Decoder(); + Metadata metadata = decoder.decode(rawId3, rawId3.length); + assertEquals(1, metadata.length()); + TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); + assertEquals("TIT2", textInformationFrame.id); + assertNull(textInformationFrame.description); + assertEquals("Hello World", textInformationFrame.value); + + // Test empty. + rawId3 = buildSingleFrameTag("TIT2", new byte[0]); + metadata = decoder.decode(rawId3, rawId3.length); + assertEquals(0, metadata.length()); + + // Test encoding byte only. + rawId3 = buildSingleFrameTag("TIT2", new byte[] {ID3_TEXT_ENCODING_UTF_8}); + metadata = decoder.decode(rawId3, rawId3.length); + assertEquals(1, metadata.length()); + textInformationFrame = (TextInformationFrame) metadata.get(0); + assertEquals("TIT2", textInformationFrame.id); + assertEquals(null, textInformationFrame.description); + assertEquals("", textInformationFrame.value); + } + + public void testDecodeWxxxFrame() throws MetadataDecoderException { + byte[] rawId3 = buildSingleFrameTag("WXXX", new byte[] {ID3_TEXT_ENCODING_UTF_8, 116, 101, 115, + 116, 0, 104, 116, 116, 112, 115, 58, 47, 47, 116, 101, 115, 116, 46, 99, 111, 109, 47, 97, + 98, 99, 63, 100, 101, 102}); + Id3Decoder decoder = new Id3Decoder(); + Metadata metadata = decoder.decode(rawId3, rawId3.length); + assertEquals(1, metadata.length()); + UrlLinkFrame urlLinkFrame = (UrlLinkFrame) metadata.get(0); + assertEquals("WXXX", urlLinkFrame.id); + assertEquals("test", urlLinkFrame.description); + assertEquals("https://test.com/abc?def", urlLinkFrame.url); + + // Test empty. + rawId3 = buildSingleFrameTag("WXXX", new byte[0]); + metadata = decoder.decode(rawId3, rawId3.length); + assertEquals(0, metadata.length()); + + // Test encoding byte only. + rawId3 = buildSingleFrameTag("WXXX", new byte[] {ID3_TEXT_ENCODING_UTF_8}); + metadata = decoder.decode(rawId3, rawId3.length); + assertEquals(1, metadata.length()); + urlLinkFrame = (UrlLinkFrame) metadata.get(0); + assertEquals("WXXX", urlLinkFrame.id); + assertEquals("", urlLinkFrame.description); + assertEquals("", urlLinkFrame.url); + } + + public void testDecodeUrlLinkFrame() throws MetadataDecoderException { + byte[] rawId3 = buildSingleFrameTag("WCOM", new byte[] {104, 116, 116, 112, 115, 58, 47, 47, + 116, 101, 115, 116, 46, 99, 111, 109, 47, 97, 98, 99, 63, 100, 101, 102}); + Id3Decoder decoder = new Id3Decoder(); + Metadata metadata = decoder.decode(rawId3, rawId3.length); + assertEquals(1, metadata.length()); + UrlLinkFrame urlLinkFrame = (UrlLinkFrame) metadata.get(0); + assertEquals("WCOM", urlLinkFrame.id); + assertEquals(null, urlLinkFrame.description); + assertEquals("https://test.com/abc?def", urlLinkFrame.url); + + // Test empty. + rawId3 = buildSingleFrameTag("WCOM", new byte[0]); + metadata = decoder.decode(rawId3, rawId3.length); + assertEquals(1, metadata.length()); + urlLinkFrame = (UrlLinkFrame) metadata.get(0); + assertEquals("WCOM", urlLinkFrame.id); + assertEquals(null, urlLinkFrame.description); + assertEquals("", urlLinkFrame.url); + } + + public void testDecodePrivFrame() throws MetadataDecoderException { + byte[] rawId3 = buildSingleFrameTag("PRIV", new byte[] {116, 101, 115, 116, 0, 1, 2, 3, 4}); + Id3Decoder decoder = new Id3Decoder(); + Metadata metadata = decoder.decode(rawId3, rawId3.length); + assertEquals(1, metadata.length()); + PrivFrame privFrame = (PrivFrame) metadata.get(0); + assertEquals("test", privFrame.owner); + MoreAsserts.assertEquals(new byte[] {1, 2, 3, 4}, privFrame.privateData); + + // Test empty. + rawId3 = buildSingleFrameTag("PRIV", new byte[0]); + metadata = decoder.decode(rawId3, rawId3.length); + assertEquals(1, metadata.length()); + privFrame = (PrivFrame) metadata.get(0); + assertEquals("", privFrame.owner); + MoreAsserts.assertEquals(new byte[0], privFrame.privateData); + } + + public void testDecodeApicFrame() throws MetadataDecoderException { + byte[] rawId3 = buildSingleFrameTag("APIC", new byte[] {3, 105, 109, 97, 103, 101, 47, 106, 112, + 101, 103, 0, 16, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 0}); + Id3Decoder decoder = new Id3Decoder(); + Metadata metadata = decoder.decode(rawId3, rawId3.length); + assertEquals(1, metadata.length()); + ApicFrame apicFrame = (ApicFrame) metadata.get(0); + assertEquals("image/jpeg", apicFrame.mimeType); + assertEquals(16, apicFrame.pictureType); + assertEquals("Hello World", apicFrame.description); + assertEquals(10, apicFrame.pictureData.length); + MoreAsserts.assertEquals(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, apicFrame.pictureData); + } + + public void testDecodeCommentFrame() throws MetadataDecoderException { + byte[] rawId3 = buildSingleFrameTag("COMM", new byte[] {ID3_TEXT_ENCODING_UTF_8, 101, 110, 103, + 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 0, 116, 101, 120, 116, 0}); + Id3Decoder decoder = new Id3Decoder(); + Metadata metadata = decoder.decode(rawId3, rawId3.length); + assertEquals(1, metadata.length()); + CommentFrame commentFrame = (CommentFrame) metadata.get(0); + assertEquals("eng", commentFrame.language); + assertEquals("description", commentFrame.description); + assertEquals("text", commentFrame.text); + + // Test empty. + rawId3 = buildSingleFrameTag("COMM", new byte[0]); + metadata = decoder.decode(rawId3, rawId3.length); + assertEquals(0, metadata.length()); + + // Test language only. + rawId3 = buildSingleFrameTag("COMM", new byte[] {ID3_TEXT_ENCODING_UTF_8, 101, 110, 103}); + metadata = decoder.decode(rawId3, rawId3.length); + assertEquals(1, metadata.length()); + commentFrame = (CommentFrame) metadata.get(0); + assertEquals("eng", commentFrame.language); + assertEquals("", commentFrame.description); + assertEquals("", commentFrame.text); + } + + private static byte[] buildSingleFrameTag(String frameId, byte[] frameData) { + byte[] frameIdBytes = frameId.getBytes(); + Assertions.checkState(frameIdBytes.length == 4); + + byte[] tagData = new byte[TAG_HEADER.length + FRAME_HEADER_LENGTH + frameData.length]; + System.arraycopy(TAG_HEADER, 0, tagData, 0, TAG_HEADER.length); + // Fill in the size part of the tag header. + int offset = TAG_HEADER.length - 4; + int tagSize = frameData.length + FRAME_HEADER_LENGTH; + tagData[offset++] = (byte) ((tagSize >> 21) & 0x7F); + tagData[offset++] = (byte) ((tagSize >> 14) & 0x7F); + tagData[offset++] = (byte) ((tagSize >> 7) & 0x7F); + tagData[offset++] = (byte) (tagSize & 0x7F); + // Fill in the frame header. + tagData[offset++] = frameIdBytes[0]; + tagData[offset++] = frameIdBytes[1]; + tagData[offset++] = frameIdBytes[2]; + tagData[offset++] = frameIdBytes[3]; + tagData[offset++] = (byte) ((frameData.length >> 24) & 0xFF); + tagData[offset++] = (byte) ((frameData.length >> 16) & 0xFF); + tagData[offset++] = (byte) ((frameData.length >> 8) & 0xFF); + tagData[offset++] = (byte) (frameData.length & 0xFF); + offset += 2; // Frame flags set to 0 + // Fill in the frame data. + System.arraycopy(frameData, 0, tagData, offset, frameData.length); + + return tagData; + } + +} diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java similarity index 98% rename from library/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java index 502fa9a789..880a214fb3 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java @@ -36,7 +36,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { public void testDecodeEmpty() throws IOException { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), EMPTY_FILE); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length); + SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); // Assert that the subtitle is empty. assertEquals(0, subtitle.getEventTimeCount()); assertTrue(subtitle.getCues(0).isEmpty()); @@ -45,7 +45,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { public void testDecodeTypical() throws IOException { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_FILE); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length); + SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); assertEquals(6, subtitle.getEventTimeCount()); assertTypicalCue1(subtitle, 0); assertTypicalCue2(subtitle, 2); @@ -55,7 +55,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { public void testDecodeTypicalWithByteOrderMark() throws IOException { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_WITH_BYTE_ORDER_MARK); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length); + SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); assertEquals(6, subtitle.getEventTimeCount()); assertTypicalCue1(subtitle, 0); assertTypicalCue2(subtitle, 2); @@ -65,7 +65,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { public void testDecodeTypicalExtraBlankLine() throws IOException { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_EXTRA_BLANK_LINE); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length); + SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); assertEquals(6, subtitle.getEventTimeCount()); assertTypicalCue1(subtitle, 0); assertTypicalCue2(subtitle, 2); @@ -76,7 +76,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { // Parsing should succeed, parsing the first and third cues only. SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_MISSING_TIMECODE); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length); + SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); assertEquals(4, subtitle.getEventTimeCount()); assertTypicalCue1(subtitle, 0); assertTypicalCue3(subtitle, 2); @@ -86,7 +86,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { // Parsing should succeed, parsing the first and third cues only. SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_MISSING_SEQUENCE); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length); + SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); assertEquals(4, subtitle.getEventTimeCount()); assertTypicalCue1(subtitle, 0); assertTypicalCue3(subtitle, 2); @@ -96,7 +96,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { // Parsing should succeed, parsing the third cue only. SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_NEGATIVE_TIMESTAMPS); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length); + SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); assertEquals(2, subtitle.getEventTimeCount()); assertTypicalCue3(subtitle, 0); } @@ -104,7 +104,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { public void testDecodeNoEndTimecodes() throws IOException { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), NO_END_TIMECODES_FILE); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length); + SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); // Test event count. assertEquals(3, subtitle.getEventTimeCount()); diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java similarity index 99% rename from library/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java index d601775009..381aaa34ae 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java @@ -482,7 +482,7 @@ public final class TtmlDecoderTest extends InstrumentationTestCase { private TtmlSubtitle getSubtitle(String file) throws IOException, SubtitleDecoderException { TtmlDecoder ttmlDecoder = new TtmlDecoder(); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), file); - return ttmlDecoder.decode(bytes, bytes.length); + return ttmlDecoder.decode(bytes, bytes.length, false); } } diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoderTest.java new file mode 100644 index 0000000000..0b24d0f275 --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoderTest.java @@ -0,0 +1,218 @@ +/* + * 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.google.android.exoplayer2.text.tx3g; + +import android.graphics.Color; +import android.graphics.Typeface; +import android.test.InstrumentationTestCase; +import android.text.SpannedString; +import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; +import android.text.style.TypefaceSpan; +import android.text.style.UnderlineSpan; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.Subtitle; +import com.google.android.exoplayer2.text.SubtitleDecoderException; +import java.io.IOException; +import java.util.Collections; + +/** + * Unit test for {@link Tx3gDecoder}. + */ +public final class Tx3gDecoderTest extends InstrumentationTestCase { + + private static final String NO_SUBTITLE = "tx3g/no_subtitle"; + private static final String SAMPLE_JUST_TEXT = "tx3g/sample_just_text"; + private static final String SAMPLE_WITH_STYL = "tx3g/sample_with_styl"; + private static final String SAMPLE_WITH_STYL_ALL_DEFAULTS = "tx3g/sample_with_styl_all_defaults"; + private static final String SAMPLE_UTF16_BE_NO_STYL = "tx3g/sample_utf16_be_no_styl"; + private static final String SAMPLE_UTF16_LE_NO_STYL = "tx3g/sample_utf16_le_no_styl"; + private static final String SAMPLE_WITH_MULTIPLE_STYL = "tx3g/sample_with_multiple_styl"; + private static final String SAMPLE_WITH_OTHER_EXTENSION = "tx3g/sample_with_other_extension"; + private static final String SAMPLE_WITH_TBOX = "tx3g/sample_with_tbox"; + private static final String INITIALIZATION = "tx3g/initialization"; + private static final String INITIALIZATION_ALL_DEFAULTS = "tx3g/initialization_all_defaults"; + + public void testDecodeNoSubtitle() throws IOException, SubtitleDecoderException { + Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); + byte[] bytes = TestUtil.getByteArray(getInstrumentation(), NO_SUBTITLE); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + assertTrue(subtitle.getCues(0).isEmpty()); + } + + public void testDecodeJustText() throws IOException, SubtitleDecoderException { + Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); + byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_JUST_TEXT); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text); + assertEquals("CC Test", text.toString()); + assertEquals(0, text.getSpans(0, text.length(), Object.class).length); + assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f); + } + + public void testDecodeWithStyl() throws IOException, SubtitleDecoderException { + Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); + byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_WITH_STYL); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text); + assertEquals("CC Test", text.toString()); + assertEquals(3, text.getSpans(0, text.length(), Object.class).length); + StyleSpan styleSpan = findSpan(text, 0, 6, StyleSpan.class); + assertEquals(Typeface.BOLD_ITALIC, styleSpan.getStyle()); + findSpan(text, 0, 6, UnderlineSpan.class); + ForegroundColorSpan colorSpan = findSpan(text, 0, 6, ForegroundColorSpan.class); + assertEquals(Color.GREEN, colorSpan.getForegroundColor()); + assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f); + } + + public void testDecodeWithStylAllDefaults() throws IOException, SubtitleDecoderException { + Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); + byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_WITH_STYL_ALL_DEFAULTS); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text); + assertEquals("CC Test", text.toString()); + assertEquals(0, text.getSpans(0, text.length(), Object.class).length); + assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f); + } + + public void testDecodeUtf16BeNoStyl() throws IOException, SubtitleDecoderException { + Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); + byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_UTF16_BE_NO_STYL); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text); + assertEquals("你好", text.toString()); + assertEquals(0, text.getSpans(0, text.length(), Object.class).length); + assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f); + } + + public void testDecodeUtf16LeNoStyl() throws IOException, SubtitleDecoderException { + Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); + byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_UTF16_LE_NO_STYL); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text); + assertEquals("你好", text.toString()); + assertEquals(0, text.getSpans(0, text.length(), Object.class).length); + assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f); + } + + public void testDecodeWithMultipleStyl() throws IOException, SubtitleDecoderException { + Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); + byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_WITH_MULTIPLE_STYL); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text); + assertEquals("Line 2\nLine 3", text.toString()); + assertEquals(4, text.getSpans(0, text.length(), Object.class).length); + StyleSpan styleSpan = findSpan(text, 0, 5, StyleSpan.class); + assertEquals(Typeface.ITALIC, styleSpan.getStyle()); + findSpan(text, 7, 12, UnderlineSpan.class); + ForegroundColorSpan colorSpan = findSpan(text, 0, 5, ForegroundColorSpan.class); + assertEquals(Color.GREEN, colorSpan.getForegroundColor()); + colorSpan = findSpan(text, 7, 12, ForegroundColorSpan.class); + assertEquals(Color.GREEN, colorSpan.getForegroundColor()); + assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f); + } + + public void testDecodeWithOtherExtension() throws IOException, SubtitleDecoderException { + Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); + byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_WITH_OTHER_EXTENSION); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text); + assertEquals("CC Test", text.toString()); + assertEquals(2, text.getSpans(0, text.length(), Object.class).length); + StyleSpan styleSpan = findSpan(text, 0, 6, StyleSpan.class); + assertEquals(Typeface.BOLD, styleSpan.getStyle()); + ForegroundColorSpan colorSpan = findSpan(text, 0, 6, ForegroundColorSpan.class); + assertEquals(Color.GREEN, colorSpan.getForegroundColor()); + assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f); + } + + public void testInitializationDecodeWithStyl() throws IOException, SubtitleDecoderException { + byte[] initBytes = TestUtil.getByteArray(getInstrumentation(), INITIALIZATION); + Tx3gDecoder decoder = new Tx3gDecoder(Collections.singletonList(initBytes)); + byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_WITH_STYL); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text); + assertEquals("CC Test", text.toString()); + assertEquals(5, text.getSpans(0, text.length(), Object.class).length); + StyleSpan styleSpan = findSpan(text, 0, text.length(), StyleSpan.class); + assertEquals(Typeface.BOLD_ITALIC, styleSpan.getStyle()); + findSpan(text, 0, text.length(), UnderlineSpan.class); + TypefaceSpan typefaceSpan = findSpan(text, 0, text.length(), TypefaceSpan.class); + assertEquals(C.SERIF_NAME, typefaceSpan.getFamily()); + ForegroundColorSpan colorSpan = findSpan(text, 0, text.length(), ForegroundColorSpan.class); + assertEquals(Color.RED, colorSpan.getForegroundColor()); + colorSpan = findSpan(text, 0, 6, ForegroundColorSpan.class); + assertEquals(Color.GREEN, colorSpan.getForegroundColor()); + assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.1f); + } + + public void testInitializationDecodeWithTbox() throws IOException, SubtitleDecoderException { + byte[] initBytes = TestUtil.getByteArray(getInstrumentation(), INITIALIZATION); + Tx3gDecoder decoder = new Tx3gDecoder(Collections.singletonList(initBytes)); + byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_WITH_TBOX); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text); + assertEquals("CC Test", text.toString()); + assertEquals(4, text.getSpans(0, text.length(), Object.class).length); + StyleSpan styleSpan = findSpan(text, 0, text.length(), StyleSpan.class); + assertEquals(Typeface.BOLD_ITALIC, styleSpan.getStyle()); + findSpan(text, 0, text.length(), UnderlineSpan.class); + TypefaceSpan typefaceSpan = findSpan(text, 0, text.length(), TypefaceSpan.class); + assertEquals(C.SERIF_NAME, typefaceSpan.getFamily()); + ForegroundColorSpan colorSpan = findSpan(text, 0, text.length(), ForegroundColorSpan.class); + assertEquals(Color.RED, colorSpan.getForegroundColor()); + assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.1875f); + } + + public void testInitializationAllDefaultsDecodeWithStyl() throws IOException, + SubtitleDecoderException { + byte[] initBytes = TestUtil.getByteArray(getInstrumentation(), INITIALIZATION_ALL_DEFAULTS); + Tx3gDecoder decoder = new Tx3gDecoder(Collections.singletonList(initBytes)); + byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_WITH_STYL); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text); + assertEquals("CC Test", text.toString()); + assertEquals(3, text.getSpans(0, text.length(), Object.class).length); + StyleSpan styleSpan = findSpan(text, 0, 6, StyleSpan.class); + assertEquals(Typeface.BOLD_ITALIC, styleSpan.getStyle()); + findSpan(text, 0, 6, UnderlineSpan.class); + ForegroundColorSpan colorSpan = findSpan(text, 0, 6, ForegroundColorSpan.class); + assertEquals(Color.GREEN, colorSpan.getForegroundColor()); + assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f); + } + + private static T findSpan(SpannedString testObject, int expectedStart, int expectedEnd, + Class expectedType) { + T[] spans = testObject.getSpans(0, testObject.length(), expectedType); + for (T span : spans) { + if (testObject.getSpanStart(span) == expectedStart + && testObject.getSpanEnd(span) == expectedEnd) { + return span; + } + } + fail("Span not found."); + return null; + } + + private static void assertFractionalLinePosition(Cue cue, float expectedFraction) { + assertEquals(Cue.LINE_TYPE_FRACTION, cue.lineType); + assertEquals(Cue.ANCHOR_TYPE_START, cue.lineAnchor); + assertTrue(Math.abs(expectedFraction - cue.line) < 1e-6); + } + +} diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java similarity index 98% rename from library/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java index a0feaea57d..2cdad081c5 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java @@ -81,14 +81,14 @@ public final class Mp4WebvttDecoderTest extends TestCase { public void testSingleCueSample() throws SubtitleDecoderException { Mp4WebvttDecoder decoder = new Mp4WebvttDecoder(); - Subtitle result = decoder.decode(SINGLE_CUE_SAMPLE, SINGLE_CUE_SAMPLE.length); + Subtitle result = decoder.decode(SINGLE_CUE_SAMPLE, SINGLE_CUE_SAMPLE.length, false); Cue expectedCue = new Cue("Hello World"); // Line feed must be trimmed by the decoder assertMp4WebvttSubtitleEquals(result, expectedCue); } public void testTwoCuesSample() throws SubtitleDecoderException { Mp4WebvttDecoder decoder = new Mp4WebvttDecoder(); - Subtitle result = decoder.decode(DOUBLE_CUE_SAMPLE, DOUBLE_CUE_SAMPLE.length); + Subtitle result = decoder.decode(DOUBLE_CUE_SAMPLE, DOUBLE_CUE_SAMPLE.length, false); Cue firstExpectedCue = new Cue("Hello World"); Cue secondExpectedCue = new Cue("Bye Bye"); assertMp4WebvttSubtitleEquals(result, firstExpectedCue, secondExpectedCue); @@ -96,7 +96,7 @@ public final class Mp4WebvttDecoderTest extends TestCase { public void testNoCueSample() throws SubtitleDecoderException { Mp4WebvttDecoder decoder = new Mp4WebvttDecoder(); - Subtitle result = decoder.decode(NO_CUE_SAMPLE, NO_CUE_SAMPLE.length); + Subtitle result = decoder.decode(NO_CUE_SAMPLE, NO_CUE_SAMPLE.length, false); assertMp4WebvttSubtitleEquals(result); } @@ -105,7 +105,7 @@ public final class Mp4WebvttDecoderTest extends TestCase { public void testSampleWithIncompleteHeader() { Mp4WebvttDecoder decoder = new Mp4WebvttDecoder(); try { - decoder.decode(INCOMPLETE_HEADER_SAMPLE, INCOMPLETE_HEADER_SAMPLE.length); + decoder.decode(INCOMPLETE_HEADER_SAMPLE, INCOMPLETE_HEADER_SAMPLE.length, false); } catch (SubtitleDecoderException e) { return; } diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java similarity index 99% rename from library/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java index 6ed0518e3c..e48a2b8b03 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java @@ -49,7 +49,7 @@ public class WebvttDecoderTest extends InstrumentationTestCase { WebvttDecoder decoder = new WebvttDecoder(); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), EMPTY_FILE); try { - decoder.decode(bytes, bytes.length); + decoder.decode(bytes, bytes.length, false); fail(); } catch (SubtitleDecoderException expected) { // Do nothing. @@ -194,7 +194,7 @@ public class WebvttDecoderTest extends InstrumentationTestCase { SubtitleDecoderException { WebvttDecoder decoder = new WebvttDecoder(); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), asset); - return decoder.decode(bytes, bytes.length); + return decoder.decode(bytes, bytes.length, false); } private Spanned getUniqueSpanTextAt(WebvttSubtitle sub, long timeUs) { diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java similarity index 94% rename from library/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java index 3200e9d6a3..38797ede66 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.upstream; +import android.net.Uri; import android.test.MoreAsserts; import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.TestUtil; @@ -88,12 +89,13 @@ public class DataSourceInputStreamTest extends TestCase { } private static DataSourceInputStream buildTestInputStream() { - FakeDataSource.Builder fakeDataSourceBuilder = new FakeDataSource.Builder() + FakeDataSource fakeDataSource = new FakeDataSource(); + fakeDataSource.getDataSet().newDefaultData() .appendReadData(Arrays.copyOfRange(TEST_DATA, 0, 5)) .appendReadData(Arrays.copyOfRange(TEST_DATA, 5, 10)) .appendReadData(Arrays.copyOfRange(TEST_DATA, 10, 15)) .appendReadData(Arrays.copyOfRange(TEST_DATA, 15, TEST_DATA.length)); - return new DataSourceInputStream(fakeDataSourceBuilder.build(), new DataSpec(null)); + return new DataSourceInputStream(fakeDataSource, new DataSpec(Uri.EMPTY)); } } diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java similarity index 95% rename from library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java index 6689d73ff1..c76e4989d8 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java @@ -20,6 +20,7 @@ import android.test.InstrumentationTestCase; import android.test.MoreAsserts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.FakeDataSource; +import com.google.android.exoplayer2.testutil.FakeDataSource.FakeData; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.FileDataSource; import com.google.android.exoplayer2.util.Util; @@ -197,12 +198,12 @@ public class CacheDataSourceTest extends InstrumentationTestCase { private CacheDataSource createCacheDataSource(boolean setReadException, boolean simulateUnknownLength, @CacheDataSource.Flags int flags, CacheDataSink cacheWriteDataSink) { - FakeDataSource.Builder builder = new FakeDataSource.Builder(); + FakeDataSource upstream = new FakeDataSource(); + FakeData fakeData = upstream.getDataSet().newDefaultData() + .setSimulateUnknownLength(simulateUnknownLength).appendReadData(TEST_DATA); if (setReadException) { - builder.appendReadError(new IOException("Shouldn't read from upstream")); + fakeData.appendReadError(new IOException("Shouldn't read from upstream")); } - FakeDataSource upstream = - builder.setSimulateUnknownLength(simulateUnknownLength).appendReadData(TEST_DATA).build(); return new CacheDataSource(simpleCache, upstream, new FileDataSource(), cacheWriteDataSink, flags, null); } diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java similarity index 97% rename from library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java index 70a7d797c1..7e8088f3be 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java @@ -139,7 +139,9 @@ public class CacheDataSourceTest2 extends AndroidTestCase { } private static FakeDataSource buildFakeUpstreamSource() { - return new FakeDataSource.Builder().appendReadData(DATA).build(); + FakeDataSource fakeDataSource = new FakeDataSource(); + fakeDataSource.getDataSet().newDefaultData().appendReadData(DATA); + return fakeDataSource; } private static CacheDataSource buildCacheDataSource(Context context, DataSource upstreamSource, diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/util/ColorParserTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ColorParserTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/util/ColorParserTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/util/ColorParserTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/util/UriUtilTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/UriUtilTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/util/UriUtilTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/util/UriUtilTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java rename to library/core/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java diff --git a/library/core/src/main/AndroidManifest.xml b/library/core/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..430930a3ca --- /dev/null +++ b/library/core/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + diff --git a/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java similarity index 95% rename from library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java rename to library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index f6aae200dd..44fb6d68ae 100644 --- a/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -254,14 +254,6 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return index; } - /** - * Use {@link #readSource(FormatHolder, DecoderInputBuffer, boolean)} instead. - */ - @Deprecated - protected final int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer) { - return readSource(formatHolder, buffer, false); - } - /** * Reads from the enabled upstream source. If the upstream source has been read to the end then * {@link C#RESULT_BUFFER_READ} is only returned if {@link #setCurrentStreamFinal()} has been @@ -296,6 +288,16 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return result; } + /** + * Attempts to skip to the keyframe before the specified position, or to the end of the stream if + * {@code positionUs} is beyond it. + * + * @param positionUs The position in microseconds. + */ + protected void skipSource(long positionUs) { + stream.skipData(positionUs - streamOffsetUs); + } + /** * Returns whether the upstream source is ready. * @@ -305,13 +307,4 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return readEndOfStream ? streamIsFinal : stream.isReady(); } - /** - * Attempts to skip to the keyframe before the specified time. - * - * @param timeUs The specified time. - */ - protected void skipToKeyframeBefore(long timeUs) { - stream.skipToKeyframeBefore(timeUs - streamOffsetUs); - } - } diff --git a/library/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java similarity index 87% rename from library/src/main/java/com/google/android/exoplayer2/C.java rename to library/core/src/main/java/com/google/android/exoplayer2/C.java index 6a1db191a0..35a69df39e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -20,6 +20,7 @@ import android.content.Context; import android.media.AudioFormat; import android.media.AudioManager; import android.media.MediaCodec; +import android.media.MediaFormat; import android.support.annotation.IntDef; import android.view.Surface; import com.google.android.exoplayer2.util.Util; @@ -76,6 +77,21 @@ public final class C { */ public static final String UTF8_NAME = "UTF-8"; + /** + * The name of the UTF-16 charset. + */ + public static final String UTF16_NAME = "UTF-16"; + + /** + * * The name of the serif font family. + */ + public static final String SERIF_NAME = "serif"; + + /** + * * The name of the sans-serif font family. + */ + public static final String SANS_SERIF_NAME = "sans-serif"; + /** * Crypto modes for a codec. */ @@ -479,15 +495,6 @@ public final class C { */ public static final int MSG_SET_VOLUME = 2; - /** - * A type of a message that can be passed to an audio {@link Renderer} via - * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object - * should be a {@link android.media.PlaybackParams}, or null, which will be used to configure the - * underlying {@link android.media.AudioTrack}. The message object should not be modified by the - * caller after it has been passed. - */ - public static final int MSG_SET_PLAYBACK_PARAMS = 3; - /** * A type of a message that can be passed to an audio {@link Renderer} via * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object @@ -500,7 +507,7 @@ public final class C { * introduce a brief gap in audio output. Note also that tracks in the same audio session must * share the same routing, so a new audio session id will be generated. */ - public static final int MSG_SET_STREAM_TYPE = 4; + public static final int MSG_SET_STREAM_TYPE = 3; /** * The type of a message that can be passed to a {@link MediaCodec}-based video {@link Renderer} @@ -510,7 +517,7 @@ public final class C { * Note that the scaling mode only applies if the {@link Surface} targeted by the renderer is * owned by a {@link android.view.SurfaceView}. */ - public static final int MSG_SET_SCALING_MODE = 5; + public static final int MSG_SET_SCALING_MODE = 4; /** * Applications or extensions may define custom {@code MSG_*} constants greater than or equal to @@ -548,11 +555,81 @@ public final class C { */ public static final int STEREO_MODE_STEREO_MESH = 3; + /** + * Video colorspaces. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({Format.NO_VALUE, COLOR_SPACE_BT709, COLOR_SPACE_BT601, COLOR_SPACE_BT2020}) + public @interface ColorSpace {} + /** + * @see MediaFormat#COLOR_STANDARD_BT709 + */ + @SuppressWarnings("InlinedApi") + public static final int COLOR_SPACE_BT709 = MediaFormat.COLOR_STANDARD_BT709; + /** + * @see MediaFormat#COLOR_STANDARD_BT601_PAL + */ + @SuppressWarnings("InlinedApi") + public static final int COLOR_SPACE_BT601 = MediaFormat.COLOR_STANDARD_BT601_PAL; + /** + * @see MediaFormat#COLOR_STANDARD_BT2020 + */ + @SuppressWarnings("InlinedApi") + public static final int COLOR_SPACE_BT2020 = MediaFormat.COLOR_STANDARD_BT2020; + + /** + * Video color transfer characteristics. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({Format.NO_VALUE, COLOR_TRANSFER_SDR, COLOR_TRANSFER_ST2084, COLOR_TRANSFER_HLG}) + public @interface ColorTransfer {} + /** + * @see MediaFormat#COLOR_TRANSFER_SDR_VIDEO + */ + @SuppressWarnings("InlinedApi") + public static final int COLOR_TRANSFER_SDR = MediaFormat.COLOR_TRANSFER_SDR_VIDEO; + /** + * @see MediaFormat#COLOR_TRANSFER_ST2084 + */ + @SuppressWarnings("InlinedApi") + public static final int COLOR_TRANSFER_ST2084 = MediaFormat.COLOR_TRANSFER_ST2084; + /** + * @see MediaFormat#COLOR_TRANSFER_HLG + */ + @SuppressWarnings("InlinedApi") + public static final int COLOR_TRANSFER_HLG = MediaFormat.COLOR_TRANSFER_HLG; + + /** + * Video color range. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({Format.NO_VALUE, COLOR_RANGE_LIMITED, COLOR_RANGE_FULL}) + public @interface ColorRange {} + /** + * @see MediaFormat#COLOR_RANGE_LIMITED + */ + @SuppressWarnings("InlinedApi") + public static final int COLOR_RANGE_LIMITED = MediaFormat.COLOR_RANGE_LIMITED; + /** + * @see MediaFormat#COLOR_RANGE_FULL + */ + @SuppressWarnings("InlinedApi") + public static final int COLOR_RANGE_FULL = MediaFormat.COLOR_RANGE_FULL; + /** * Priority for media playback. + * + *

Larger values indicate higher priorities. */ public static final int PRIORITY_PLAYBACK = 0; + /** + * Priority for media downloading. + * + *

Larger values indicate higher priorities. + */ + public static final int PRIORITY_DOWNLOAD = PRIORITY_PLAYBACK - 1000; + /** * Converts a time in microseconds to the corresponding time in milliseconds, preserving * {@link #TIME_UNSET} values. diff --git a/library/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java rename to library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java new file mode 100644 index 0000000000..3e7cb8a68b --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java @@ -0,0 +1,327 @@ +/* + * 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.google.android.exoplayer2; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.IntDef; +import android.util.Log; +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.MetadataRenderer; +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; + +/** + * Default {@link RenderersFactory} implementation. + */ +public class DefaultRenderersFactory implements RenderersFactory { + + /** + * The default maximum duration for which a video renderer can attempt to seamlessly join an + * ongoing playback. + */ + public static final long DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS = 5000; + + /** + * Modes for using extension renderers. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({EXTENSION_RENDERER_MODE_OFF, EXTENSION_RENDERER_MODE_ON, + EXTENSION_RENDERER_MODE_PREFER}) + public @interface ExtensionRendererMode {} + /** + * Do not allow use of extension renderers. + */ + public 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. + */ + public 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. + */ + public static final int EXTENSION_RENDERER_MODE_PREFER = 2; + + private static final String TAG = "DefaultRenderersFactory"; + + protected static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50; + + private final Context context; + private final DrmSessionManager drmSessionManager; + private final @ExtensionRendererMode int extensionRendererMode; + private final long allowedVideoJoiningTimeMs; + + /** + * @param context A {@link Context}. + */ + public DefaultRenderersFactory(Context context) { + this(context, null); + } + + /** + * @param context A {@link Context}. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if DRM protected + * playbacks are not required. + */ + public DefaultRenderersFactory(Context context, + DrmSessionManager drmSessionManager) { + this(context, drmSessionManager, EXTENSION_RENDERER_MODE_OFF); + } + + /** + * @param context A {@link Context}. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if DRM protected + * playbacks are not required.. + * @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. + */ + public DefaultRenderersFactory(Context context, + DrmSessionManager drmSessionManager, + @ExtensionRendererMode int extensionRendererMode) { + this(context, drmSessionManager, extensionRendererMode, + DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS); + } + + /** + * @param context A {@link Context}. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if DRM protected + * playbacks are not required.. + * @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. + */ + public DefaultRenderersFactory(Context context, + DrmSessionManager drmSessionManager, + @ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs) { + this.context = context; + this.drmSessionManager = drmSessionManager; + this.extensionRendererMode = extensionRendererMode; + this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs; + } + + @Override + public Renderer[] createRenderers(Handler eventHandler, + VideoRendererEventListener videoRendererEventListener, + AudioRendererEventListener audioRendererEventListener, + TextRenderer.Output textRendererOutput, MetadataRenderer.Output metadataRendererOutput) { + ArrayList renderersList = new ArrayList<>(); + buildVideoRenderers(context, drmSessionManager, allowedVideoJoiningTimeMs, + eventHandler, videoRendererEventListener, extensionRendererMode, renderersList); + buildAudioRenderers(context, drmSessionManager, buildAudioProcessors(), + eventHandler, audioRendererEventListener, extensionRendererMode, renderersList); + buildTextRenderers(context, textRendererOutput, eventHandler.getLooper(), + extensionRendererMode, renderersList); + buildMetadataRenderers(context, metadataRendererOutput, eventHandler.getLooper(), + extensionRendererMode, renderersList); + buildMiscellaneousRenderers(context, eventHandler, extensionRendererMode, renderersList); + 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 out An array to which the built renderers should be appended. + */ + protected void buildVideoRenderers(Context context, + DrmSessionManager drmSessionManager, long allowedVideoJoiningTimeMs, + Handler eventHandler, VideoRendererEventListener eventListener, + @ExtensionRendererMode int extensionRendererMode, ArrayList out) { + out.add(new MediaCodecVideoRenderer(context, MediaCodecSelector.DEFAULT, + allowedVideoJoiningTimeMs, drmSessionManager, false, eventHandler, eventListener, + MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)); + + if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { + return; + } + int extensionRendererIndex = out.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(true, allowedVideoJoiningTimeMs, + eventHandler, eventListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY); + out.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 RuntimeException(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 out An array to which the built renderers should be appended. + */ + protected void buildAudioRenderers(Context context, + DrmSessionManager drmSessionManager, + AudioProcessor[] audioProcessors, Handler eventHandler, + AudioRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode, + ArrayList out) { + out.add(new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT, drmSessionManager, true, + eventHandler, eventListener, AudioCapabilities.getCapabilities(context), audioProcessors)); + + if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { + return; + } + int extensionRendererIndex = out.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); + out.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 RuntimeException(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); + out.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 RuntimeException(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); + out.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 RuntimeException(e); + } + } + + /** + * Builds text renderers for use by the player. + * + * @param context The {@link Context} associated with 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 extensionRendererMode The extension renderer mode. + * @param out An array to which the built renderers should be appended. + */ + protected void buildTextRenderers(Context context, TextRenderer.Output output, + Looper outputLooper, @ExtensionRendererMode int extensionRendererMode, + ArrayList out) { + out.add(new TextRenderer(output, outputLooper)); + } + + /** + * Builds metadata renderers for use by the player. + * + * @param context The {@link Context} associated with 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 extensionRendererMode The extension renderer mode. + * @param out An array to which the built renderers should be appended. + */ + protected void buildMetadataRenderers(Context context, MetadataRenderer.Output output, + Looper outputLooper, @ExtensionRendererMode int extensionRendererMode, + ArrayList out) { + out.add(new MetadataRenderer(output, outputLooper)); + } + + /** + * Builds any miscellaneous renderers used by the player. + * + * @param context The {@link Context} associated with the player. + * @param eventHandler A handler to use when invoking event listeners and outputs. + * @param extensionRendererMode The extension renderer mode. + * @param out An array to which the built renderers should be appended. + */ + protected void buildMiscellaneousRenderers(Context context, Handler eventHandler, + @ExtensionRendererMode int extensionRendererMode, ArrayList out) { + // Do nothing. + } + + /** + * Builds an array of {@link AudioProcessor}s that will process PCM audio before output. + */ + protected AudioProcessor[] buildAudioProcessors() { + return new AudioProcessor[0]; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java rename to library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java similarity index 89% rename from library/src/main/java/com/google/android/exoplayer2/ExoPlayer.java rename to library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index 083569416c..ab521e3733 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; @@ -23,9 +24,6 @@ import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.SingleSampleMediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.text.TextRenderer; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -47,12 +45,11 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; *

    *
  • A {@link MediaSource} that defines the media to be played, loads the media, and from * which the loaded media can be read. A MediaSource is injected via {@link #prepare} at the start - * of playback. The library provides default implementations for regular media files - * ({@link ExtractorMediaSource}), DASH ({@link DashMediaSource}), SmoothStreaming - * ({@link SsMediaSource}) and HLS ({@link HlsMediaSource}), implementations for merging - * ({@link MergingMediaSource}) and concatenating ({@link ConcatenatingMediaSource}) other - * MediaSources, and an implementation for loading single samples - * ({@link SingleSampleMediaSource}) most often used for side-loaded subtitle and closed + * of playback. The library modules provide default implementations for regular media files + * ({@link ExtractorMediaSource}), DASH (DashMediaSource), SmoothStreaming (SsMediaSource) and HLS + * (HlsMediaSource), implementations for merging ({@link MergingMediaSource}) and concatenating + * ({@link ConcatenatingMediaSource}) other MediaSources, and an implementation for loading single + * samples ({@link SingleSampleMediaSource}) most often used for side-loaded subtitle and closed * caption files.
  • *
  • {@link Renderer}s that render individual components of the media. The library * provides default implementations for common media types ({@link MediaCodecVideoRenderer}, @@ -172,6 +169,16 @@ public interface ExoPlayer { */ void onPositionDiscontinuity(); + /** + * Called when the current playback parameters change. The playback parameters may change due to + * a call to {@link ExoPlayer#setPlaybackParameters(PlaybackParameters)}, or the player itself + * may change them (for example, if audio playback switches to passthrough mode, where speed + * adjustment is no longer possible). + * + * @param playbackParameters The playback parameters. + */ + void onPlaybackParametersChanged(PlaybackParameters playbackParameters); + } /** @@ -344,6 +351,28 @@ public interface ExoPlayer { */ void seekTo(int windowIndex, long positionMs); + /** + * Attempts to set the playback parameters. Passing {@code null} sets the parameters to the + * default, {@link PlaybackParameters#DEFAULT}, which means there is no speed or pitch adjustment. + *

    + * Playback parameters changes may cause the player to buffer. + * {@link EventListener#onPlaybackParametersChanged(PlaybackParameters)} will be called whenever + * the currently active playback parameters change. When that listener is called, the parameters + * passed to it may not match {@code playbackParameters}. For example, the chosen speed or pitch + * may be out of range, in which case they are constrained to a set of permitted values. If it is + * not possible to change the playback parameters, the listener will not be invoked. + * + * @param playbackParameters The playback parameters, or {@code null} to use the defaults. + */ + void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters); + + /** + * Returns the currently active playback parameters. + * + * @see EventListener#onPlaybackParametersChanged(PlaybackParameters) + */ + PlaybackParameters getPlaybackParameters(); + /** * Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention * is to pause playback. diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java similarity index 65% rename from library/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java rename to library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java index 43de6fe751..7aecd20d4e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java @@ -26,12 +26,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelector; */ public final class ExoPlayerFactory { - /** - * The default maximum duration for which a video renderer can attempt to seamlessly join an - * ongoing playback. - */ - public static final long DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS = 5000; - private ExoPlayerFactory() {} /** @@ -41,10 +35,13 @@ public final class ExoPlayerFactory { * @param context A {@link Context}. * @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance. + * @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}. */ + @Deprecated public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, LoadControl loadControl) { - return newSimpleInstance(context, trackSelector, loadControl, null); + RenderersFactory renderersFactory = new DefaultRenderersFactory(context); + return newSimpleInstance(renderersFactory, trackSelector, loadControl); } /** @@ -56,11 +53,13 @@ public final class ExoPlayerFactory { * @param loadControl The {@link LoadControl} that will be used by the instance. * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance * will not be used for DRM protected playbacks. + * @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}. */ + @Deprecated public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, LoadControl loadControl, DrmSessionManager drmSessionManager) { - return newSimpleInstance(context, trackSelector, loadControl, - drmSessionManager, SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF); + RenderersFactory renderersFactory = new DefaultRenderersFactory(context, drmSessionManager); + return newSimpleInstance(renderersFactory, trackSelector, loadControl); } /** @@ -75,12 +74,15 @@ public final class ExoPlayerFactory { * @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. + * @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}. */ + @Deprecated public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, LoadControl loadControl, DrmSessionManager drmSessionManager, - @SimpleExoPlayer.ExtensionRendererMode int extensionRendererMode) { - return newSimpleInstance(context, trackSelector, loadControl, drmSessionManager, - extensionRendererMode, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS); + @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode) { + RenderersFactory renderersFactory = new DefaultRenderersFactory(context, drmSessionManager, + extensionRendererMode); + return newSimpleInstance(renderersFactory, trackSelector, loadControl); } /** @@ -97,13 +99,52 @@ public final class ExoPlayerFactory { * build for them to be considered available. * @param allowedVideoJoiningTimeMs The maximum duration for which a video renderer can attempt to * seamlessly join an ongoing playback. + * @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}. */ + @Deprecated public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, LoadControl loadControl, DrmSessionManager drmSessionManager, - @SimpleExoPlayer.ExtensionRendererMode int extensionRendererMode, + @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs) { - return new SimpleExoPlayer(context, trackSelector, loadControl, drmSessionManager, + RenderersFactory renderersFactory = new DefaultRenderersFactory(context, drmSessionManager, extensionRendererMode, allowedVideoJoiningTimeMs); + return newSimpleInstance(renderersFactory, trackSelector, loadControl); + } + + /** + * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated + * {@link Looper}. + * + * @param context A {@link Context}. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + */ + public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector) { + return newSimpleInstance(new DefaultRenderersFactory(context), trackSelector); + } + + /** + * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated + * {@link Looper}. + * + * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + */ + public static SimpleExoPlayer newSimpleInstance(RenderersFactory renderersFactory, + TrackSelector trackSelector) { + return newSimpleInstance(renderersFactory, trackSelector, new DefaultLoadControl()); + } + + /** + * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated + * {@link Looper}. + * + * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + * @param loadControl The {@link LoadControl} that will be used by the instance. + */ + public static SimpleExoPlayer newSimpleInstance(RenderersFactory renderersFactory, + TrackSelector trackSelector, LoadControl loadControl) { + return new SimpleExoPlayer(renderersFactory, trackSelector, loadControl); } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java similarity index 78% rename from library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java rename to library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index d44d138091..4131b97954 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -19,6 +19,7 @@ import android.annotation.SuppressLint; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.ExoPlayerImplInternal.PlaybackInfo; import com.google.android.exoplayer2.ExoPlayerImplInternal.SourceInfo; @@ -52,17 +53,20 @@ import java.util.concurrent.CopyOnWriteArraySet; private boolean playWhenReady; private int playbackState; private int pendingSeekAcks; + private int pendingPrepareAcks; private boolean isLoading; private Timeline timeline; private Object manifest; private TrackGroupArray trackGroups; private TrackSelectionArray trackSelections; + private PlaybackParameters playbackParameters; // Playback information when there is no pending seek/set source operation. private PlaybackInfo playbackInfo; // Playback information when there is a pending seek/set source operation. private int maskingWindowIndex; + private int maskingPeriodIndex; private long maskingWindowPositionMs; /** @@ -74,7 +78,7 @@ import java.util.concurrent.CopyOnWriteArraySet; */ @SuppressLint("HandlerLeak") public ExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) { - Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION + " [" + Util.DEVICE_DEBUG_INFO + "]"); + Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION_SLASHY + " [" + Util.DEVICE_DEBUG_INFO + "]"); Assertions.checkState(renderers.length > 0); this.renderers = Assertions.checkNotNull(renderers); this.trackSelector = Assertions.checkNotNull(trackSelector); @@ -87,6 +91,7 @@ import java.util.concurrent.CopyOnWriteArraySet; period = new Timeline.Period(); trackGroups = TrackGroupArray.EMPTY; trackSelections = emptyTrackSelections; + playbackParameters = PlaybackParameters.DEFAULT; eventHandler = new Handler() { @Override public void handleMessage(Message msg) { @@ -138,6 +143,7 @@ import java.util.concurrent.CopyOnWriteArraySet; } } } + pendingPrepareAcks++; internalPlayer.prepare(mediaSource, resetPosition); } @@ -184,6 +190,22 @@ import java.util.concurrent.CopyOnWriteArraySet; } pendingSeekAcks++; maskingWindowIndex = windowIndex; + if (timeline.isEmpty()) { + maskingPeriodIndex = 0; + } else { + timeline.getWindow(windowIndex, window); + long resolvedPositionMs = + positionMs == C.TIME_UNSET ? window.getDefaultPositionUs() : positionMs; + int periodIndex = window.firstPeriodIndex; + long periodPositionUs = window.getPositionInFirstPeriodUs() + C.msToUs(resolvedPositionMs); + long periodDurationUs = timeline.getPeriod(periodIndex, period).getDurationUs(); + while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs + && periodIndex < window.lastPeriodIndex) { + periodPositionUs -= periodDurationUs; + periodDurationUs = timeline.getPeriod(++periodIndex, period).getDurationUs(); + } + maskingPeriodIndex = periodIndex; + } if (positionMs == C.TIME_UNSET) { maskingWindowPositionMs = 0; internalPlayer.seekTo(timeline, windowIndex, C.TIME_UNSET); @@ -196,6 +218,19 @@ import java.util.concurrent.CopyOnWriteArraySet; } } + @Override + public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { + if (playbackParameters == null) { + playbackParameters = PlaybackParameters.DEFAULT; + } + internalPlayer.setPlaybackParameters(playbackParameters); + } + + @Override + public PlaybackParameters getPlaybackParameters() { + return playbackParameters; + } + @Override public void stop() { internalPlayer.stop(); @@ -219,7 +254,11 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public int getCurrentPeriodIndex() { - return playbackInfo.periodIndex; + if (timeline.isEmpty() || pendingSeekAcks > 0) { + return maskingPeriodIndex; + } else { + return playbackInfo.periodIndex; + } } @Override @@ -273,18 +312,12 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public boolean isCurrentWindowDynamic() { - if (timeline.isEmpty()) { - return false; - } - return timeline.getWindow(getCurrentWindowIndex(), window).isDynamic; + return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isDynamic; } @Override public boolean isCurrentWindowSeekable() { - if (timeline.isEmpty()) { - return false; - } - return timeline.getWindow(getCurrentWindowIndex(), window).isSeekable; + return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isSeekable; } @Override @@ -320,6 +353,10 @@ import java.util.concurrent.CopyOnWriteArraySet; // Not private so it can be called from an inner class without going through a thunk method. /* package */ void handleEvent(Message msg) { switch (msg.what) { + case ExoPlayerImplInternal.MSG_PREPARE_ACK: { + pendingPrepareAcks--; + break; + } case ExoPlayerImplInternal.MSG_STATE_CHANGED: { playbackState = msg.arg1; for (EventListener listener : listeners) { @@ -335,13 +372,15 @@ import java.util.concurrent.CopyOnWriteArraySet; break; } case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: { - TrackSelectorResult trackSelectorResult = (TrackSelectorResult) msg.obj; - tracksSelected = true; - trackGroups = trackSelectorResult.groups; - trackSelections = trackSelectorResult.selections; - trackSelector.onSelectionActivated(trackSelectorResult.info); - for (EventListener listener : listeners) { - listener.onTracksChanged(trackGroups, trackSelections); + if (pendingPrepareAcks == 0) { + TrackSelectorResult trackSelectorResult = (TrackSelectorResult) msg.obj; + tracksSelected = true; + trackGroups = trackSelectorResult.groups; + trackSelections = trackSelectorResult.selections; + trackSelector.onSelectionActivated(trackSelectorResult.info); + for (EventListener listener : listeners) { + listener.onTracksChanged(trackGroups, trackSelections); + } } break; } @@ -367,12 +406,24 @@ import java.util.concurrent.CopyOnWriteArraySet; } case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: { SourceInfo sourceInfo = (SourceInfo) msg.obj; - timeline = sourceInfo.timeline; - manifest = sourceInfo.manifest; - playbackInfo = sourceInfo.playbackInfo; pendingSeekAcks -= sourceInfo.seekAcks; - for (EventListener listener : listeners) { - listener.onTimelineChanged(timeline, manifest); + if (pendingPrepareAcks == 0) { + timeline = sourceInfo.timeline; + manifest = sourceInfo.manifest; + playbackInfo = sourceInfo.playbackInfo; + for (EventListener listener : listeners) { + listener.onTimelineChanged(timeline, manifest); + } + } + break; + } + case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED: { + PlaybackParameters playbackParameters = (PlaybackParameters) msg.obj; + if (!this.playbackParameters.equals(playbackParameters)) { + this.playbackParameters = playbackParameters; + for (EventListener listener : listeners) { + listener.onPlaybackParametersChanged(playbackParameters); + } } break; } @@ -383,6 +434,8 @@ import java.util.concurrent.CopyOnWriteArraySet; } break; } + default: + throw new IllegalStateException(); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java similarity index 96% rename from library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java rename to library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index e4c109e85b..bf5b3f6482 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -32,7 +32,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MediaClock; -import com.google.android.exoplayer2.util.PriorityHandlerThread; import com.google.android.exoplayer2.util.StandaloneMediaClock; import com.google.android.exoplayer2.util.TraceUtil; import java.io.IOException; @@ -90,26 +89,29 @@ import java.io.IOException; private static final String TAG = "ExoPlayerImplInternal"; // External messages + public static final int MSG_PREPARE_ACK = 0; public static final int MSG_STATE_CHANGED = 1; public static final int MSG_LOADING_CHANGED = 2; public static final int MSG_TRACKS_CHANGED = 3; public static final int MSG_SEEK_ACK = 4; public static final int MSG_POSITION_DISCONTINUITY = 5; public static final int MSG_SOURCE_INFO_REFRESHED = 6; - public static final int MSG_ERROR = 7; + public static final int MSG_PLAYBACK_PARAMETERS_CHANGED = 7; + public static final int MSG_ERROR = 8; // Internal messages private static final int MSG_PREPARE = 0; private static final int MSG_SET_PLAY_WHEN_READY = 1; private static final int MSG_DO_SOME_WORK = 2; private static final int MSG_SEEK_TO = 3; - private static final int MSG_STOP = 4; - private static final int MSG_RELEASE = 5; - private static final int MSG_REFRESH_SOURCE_INFO = 6; - private static final int MSG_PERIOD_PREPARED = 7; - private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 8; - private static final int MSG_TRACK_SELECTION_INVALIDATED = 9; - private static final int MSG_CUSTOM = 10; + private static final int MSG_SET_PLAYBACK_PARAMETERS = 4; + private static final int MSG_STOP = 5; + private static final int MSG_RELEASE = 6; + private static final int MSG_REFRESH_SOURCE_INFO = 7; + private static final int MSG_PERIOD_PREPARED = 8; + private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 9; + private static final int MSG_TRACK_SELECTION_INVALIDATED = 10; + private static final int MSG_CUSTOM = 11; private static final int PREPARING_SOURCE_INTERVAL_MS = 10; private static final int RENDERING_INTERVAL_MS = 10; @@ -143,6 +145,7 @@ import java.io.IOException; private final Timeline.Period period; private PlaybackInfo playbackInfo; + private PlaybackParameters playbackParameters; private Renderer rendererMediaClockSource; private MediaClock rendererMediaClock; private MediaSource mediaSource; @@ -188,10 +191,11 @@ import java.io.IOException; window = new Timeline.Window(); period = new Timeline.Period(); trackSelector.init(this); + playbackParameters = PlaybackParameters.DEFAULT; // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can // not normally change to this priority" is incorrect. - internalPlaybackThread = new PriorityHandlerThread("ExoPlayerImplInternal:Handler", + internalPlaybackThread = new HandlerThread("ExoPlayerImplInternal:Handler", Process.THREAD_PRIORITY_AUDIO); internalPlaybackThread.start(); handler = new Handler(internalPlaybackThread.getLooper(), this); @@ -211,6 +215,10 @@ import java.io.IOException; .sendToTarget(); } + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + handler.obtainMessage(MSG_SET_PLAYBACK_PARAMETERS, playbackParameters).sendToTarget(); + } + public void stop() { handler.sendEmptyMessage(MSG_STOP); } @@ -304,6 +312,10 @@ import java.io.IOException; seekToInternal((SeekPosition) msg.obj); return true; } + case MSG_SET_PLAYBACK_PARAMETERS: { + setPlaybackParametersInternal((PlaybackParameters) msg.obj); + return true; + } case MSG_STOP: { stopInternal(); return true; @@ -371,6 +383,7 @@ import java.io.IOException; } private void prepareInternal(MediaSource mediaSource, boolean resetPosition) { + eventHandler.sendEmptyMessage(MSG_PREPARE_ACK); resetInternal(true); loadControl.onPrepared(); if (resetPosition) { @@ -478,6 +491,19 @@ import java.io.IOException; maybeThrowPeriodPrepareError(); } + // The standalone media clock never changes playback parameters, so just check the renderer. + if (rendererMediaClock != null) { + PlaybackParameters playbackParameters = rendererMediaClock.getPlaybackParameters(); + if (!playbackParameters.equals(this.playbackParameters)) { + // TODO: Make LoadControl, period transition position projection, adaptive track selection + // and potentially any time-related code in renderers take into account the playback speed. + this.playbackParameters = playbackParameters; + standaloneMediaClock.synchronize(rendererMediaClock); + eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters) + .sendToTarget(); + } + } + long playingPeriodDurationUs = timeline.getPeriod(playingPeriodHolder.index, period) .getDurationUs(); if (allRenderersEnded @@ -646,6 +672,14 @@ import java.io.IOException; } } + private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) { + playbackParameters = rendererMediaClock != null + ? rendererMediaClock.setPlaybackParameters(playbackParameters) + : standaloneMediaClock.setPlaybackParameters(playbackParameters); + this.playbackParameters = playbackParameters; + eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters).sendToTarget(); + } + private void stopInternal() { resetInternal(true); loadControl.onStopped(); @@ -774,7 +808,7 @@ import java.io.IOException; if (sampleStream == null) { // The renderer won't be re-enabled. Sync standaloneMediaClock so that it can take // over timing responsibilities. - standaloneMediaClock.setPositionUs(rendererMediaClock.getPositionUs()); + standaloneMediaClock.synchronize(rendererMediaClock); } rendererMediaClock = null; rendererMediaClockSource = null; @@ -1334,7 +1368,7 @@ import java.io.IOException; // is final and it's not reading ahead. if (renderer == rendererMediaClockSource) { // Sync standaloneMediaClock so that it can take over timing responsibilities. - standaloneMediaClock.setPositionUs(rendererMediaClock.getPositionUs()); + standaloneMediaClock.synchronize(rendererMediaClock); rendererMediaClock = null; rendererMediaClockSource = null; } @@ -1380,6 +1414,7 @@ import java.io.IOException; } rendererMediaClock = mediaClock; rendererMediaClockSource = renderer; + rendererMediaClock.setPlaybackParameters(playbackParameters); } // Start the renderer if playing. if (playing) { diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java similarity index 67% rename from library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java rename to library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index bee9904590..13cf35d449 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -21,18 +21,26 @@ package com.google.android.exoplayer2; public interface ExoPlayerLibraryInfo { /** - * The version of the library, expressed as a string. + * The version of the library expressed as a string, for example "1.2.3". */ - String VERSION = "2.3.1"; + // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. + String VERSION = "2.4.0"; /** - * The version of the library, expressed as an integer. + * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. + */ + // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. + String VERSION_SLASHY = "ExoPlayerLib/2.4.0"; + + /** + * The version of the library expressed as an integer, for example 1002003. *

    * Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the * corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding * integer version 123045006 (123-045-006). */ - int VERSION_INT = 2003001; + // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. + int VERSION_INT = 2004000; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} diff --git a/library/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java similarity index 86% rename from library/src/main/java/com/google/android/exoplayer2/Format.java rename to library/core/src/main/java/com/google/android/exoplayer2/Format.java index 866e512288..0bffd28ba5 100644 --- a/library/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.video.ColorInfo; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; @@ -128,6 +129,10 @@ public final class Format implements Parcelable { * The projection data for 360/VR video, or null if not applicable. */ public final byte[] projectionData; + /** + * The color metadata associated with the video, helps with accurate color reproduction. + */ + public final ColorInfo colorInfo; // Audio specific. @@ -192,7 +197,7 @@ public final class Format implements Parcelable { String sampleMimeType, String codecs, int bitrate, int width, int height, float frameRate, List initializationData, @C.SelectionFlags int selectionFlags) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, width, - height, frameRate, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, + height, frameRate, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, selectionFlags, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, initializationData, null, null); } @@ -210,17 +215,18 @@ public final class Format implements Parcelable { DrmInitData drmInitData) { return createVideoSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, initializationData, rotationDegrees, pixelWidthHeightRatio, null, - NO_VALUE, drmInitData); + NO_VALUE, null, drmInitData); } public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs, int bitrate, int maxInputSize, int width, int height, float frameRate, List initializationData, int rotationDegrees, float pixelWidthHeightRatio, - byte[] projectionData, @C.StereoMode int stereoMode, DrmInitData drmInitData) { + byte[] projectionData, @C.StereoMode int stereoMode, ColorInfo colorInfo, + DrmInitData drmInitData) { return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, width, height, - frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, - initializationData, drmInitData, null); + frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, + colorInfo, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, null, NO_VALUE, + OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, null); } // Audio. @@ -229,8 +235,8 @@ public final class Format implements Parcelable { String sampleMimeType, String codecs, int bitrate, int channelCount, int sampleRate, List initializationData, @C.SelectionFlags int selectionFlags, String language) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, channelCount, sampleRate, NO_VALUE, - NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, + NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, channelCount, sampleRate, + NO_VALUE, NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, initializationData, null, null); } @@ -257,7 +263,7 @@ public final class Format implements Parcelable { List initializationData, DrmInitData drmInitData, @C.SelectionFlags int selectionFlags, String language, Metadata metadata) { return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, channelCount, sampleRate, pcmEncoding, + NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, metadata); } @@ -275,38 +281,39 @@ public final class Format implements Parcelable { String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags, String language, int accessibilityChannel) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, selectionFlags, language, accessibilityChannel, + NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, + NO_VALUE, NO_VALUE, NO_VALUE, selectionFlags, language, accessibilityChannel, OFFSET_SAMPLE_RELATIVE, null, null, null); } public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData) { return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language, - NO_VALUE, drmInitData, OFFSET_SAMPLE_RELATIVE); + NO_VALUE, drmInitData, OFFSET_SAMPLE_RELATIVE, Collections.emptyList()); } public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags, String language, int accessibilityChannel, DrmInitData drmInitData) { return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language, - accessibilityChannel, drmInitData, OFFSET_SAMPLE_RELATIVE); + accessibilityChannel, drmInitData, OFFSET_SAMPLE_RELATIVE, Collections.emptyList()); } public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData, long subsampleOffsetUs) { return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language, - NO_VALUE, drmInitData, subsampleOffsetUs); + NO_VALUE, drmInitData, subsampleOffsetUs, Collections.emptyList()); } public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags, String language, - int accessibilityChannel, DrmInitData drmInitData, long subsampleOffsetUs) { + int accessibilityChannel, DrmInitData drmInitData, long subsampleOffsetUs, + List initializationData) { return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, null, - drmInitData, null); + NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, + NO_VALUE, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, + initializationData, drmInitData, null); } // Image. @@ -314,7 +321,7 @@ public final class Format implements Parcelable { public static Format createImageSampleFormat(String id, String sampleMimeType, String codecs, int bitrate, List initializationData, String language, DrmInitData drmInitData) { return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, + NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, null); } @@ -325,7 +332,7 @@ public final class Format implements Parcelable { String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags, String language) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, + NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, null, null); } @@ -333,22 +340,22 @@ public final class Format implements Parcelable { public static Format createSampleFormat(String id, String sampleMimeType, long subsampleOffsetUs) { return new Format(id, null, sampleMimeType, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, + NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, null, NO_VALUE, subsampleOffsetUs, null, null, null); } public static Format createSampleFormat(String id, String sampleMimeType, String codecs, int bitrate, DrmInitData drmInitData) { return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, + NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, drmInitData, null); } /* package */ Format(String id, String containerMimeType, String sampleMimeType, String codecs, int bitrate, int maxInputSize, int width, int height, float frameRate, int rotationDegrees, float pixelWidthHeightRatio, byte[] projectionData, @C.StereoMode int stereoMode, - int channelCount, int sampleRate, @C.PcmEncoding int pcmEncoding, int encoderDelay, - int encoderPadding, @C.SelectionFlags int selectionFlags, String language, + ColorInfo colorInfo, int channelCount, int sampleRate, @C.PcmEncoding int pcmEncoding, + int encoderDelay, int encoderPadding, @C.SelectionFlags int selectionFlags, String language, int accessibilityChannel, long subsampleOffsetUs, List initializationData, DrmInitData drmInitData, Metadata metadata) { this.id = id; @@ -364,6 +371,7 @@ public final class Format implements Parcelable { this.pixelWidthHeightRatio = pixelWidthHeightRatio; this.projectionData = projectionData; this.stereoMode = stereoMode; + this.colorInfo = colorInfo; this.channelCount = channelCount; this.sampleRate = sampleRate; this.pcmEncoding = pcmEncoding; @@ -395,6 +403,7 @@ public final class Format implements Parcelable { boolean hasProjectionData = in.readInt() != 0; projectionData = hasProjectionData ? in.createByteArray() : null; stereoMode = in.readInt(); + colorInfo = in.readParcelable(ColorInfo.class.getClassLoader()); channelCount = in.readInt(); sampleRate = in.readInt(); pcmEncoding = in.readInt(); @@ -416,28 +425,29 @@ public final class Format implements Parcelable { public Format copyWithMaxInputSize(int maxInputSize) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, - stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, - drmInitData, metadata); + stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, + encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, + initializationData, drmInitData, metadata); } public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, - stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, - drmInitData, metadata); + stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, + encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, + initializationData, drmInitData, metadata); } public Format copyWithContainerInfo(String id, String codecs, int bitrate, int width, int height, @C.SelectionFlags int selectionFlags, String language) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, - stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, - drmInitData, metadata); + stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, + encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, + initializationData, drmInitData, metadata); } + @SuppressWarnings("ReferenceEquality") public Format copyWithManifestFormatInfo(Format manifestFormat) { if (this == manifestFormat) { // No need to copy from ourselves. @@ -453,33 +463,33 @@ public final class Format implements Parcelable { : this.drmInitData; return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, - channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags, - language, accessibilityChannel, subsampleOffsetUs, initializationData, drmInitData, - metadata); + colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, + selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, + drmInitData, metadata); } public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, - stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, - drmInitData, metadata); + stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, + encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, + initializationData, drmInitData, metadata); } public Format copyWithDrmInitData(DrmInitData drmInitData) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, - stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, - drmInitData, metadata); + stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, + encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, + initializationData, drmInitData, metadata); } public Format copyWithMetadata(Metadata metadata) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, - stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, - drmInitData, metadata); + stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, + encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, + initializationData, drmInitData, metadata); } /** @@ -511,6 +521,7 @@ public final class Format implements Parcelable { for (int i = 0; i < initializationData.size(); i++) { format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i))); } + maybeSetColorInfoV24(format, colorInfo); return format; } @@ -567,6 +578,7 @@ public final class Format implements Parcelable { || !Util.areEqual(codecs, other.codecs) || !Util.areEqual(drmInitData, other.drmInitData) || !Util.areEqual(metadata, other.metadata) + || !Util.areEqual(colorInfo, other.colorInfo) || !Arrays.equals(projectionData, other.projectionData) || initializationData.size() != other.initializationData.size()) { return false; @@ -579,6 +591,17 @@ public final class Format implements Parcelable { return true; } + @TargetApi(24) + private static void maybeSetColorInfoV24(MediaFormat format, ColorInfo colorInfo) { + if (colorInfo == null) { + return; + } + maybeSetIntegerV16(format, MediaFormat.KEY_COLOR_TRANSFER, colorInfo.colorTransfer); + maybeSetIntegerV16(format, MediaFormat.KEY_COLOR_STANDARD, colorInfo.colorSpace); + maybeSetIntegerV16(format, MediaFormat.KEY_COLOR_RANGE, colorInfo.colorRange); + maybeSetByteBufferV16(format, MediaFormat.KEY_HDR_STATIC_INFO, colorInfo.hdrStaticInfo); + } + @TargetApi(16) private static void maybeSetStringV16(MediaFormat format, String key, String value) { if (value != null) { @@ -600,6 +623,13 @@ public final class Format implements Parcelable { } } + @TargetApi(16) + private static void maybeSetByteBufferV16(MediaFormat format, String key, byte[] value) { + if (value != null) { + format.setByteBuffer(key, ByteBuffer.wrap(value)); + } + } + // Utility methods /** @@ -657,6 +687,7 @@ public final class Format implements Parcelable { dest.writeByteArray(projectionData); } dest.writeInt(stereoMode); + dest.writeParcelable(colorInfo, flags); dest.writeInt(channelCount); dest.writeInt(sampleRate); dest.writeInt(pcmEncoding); diff --git a/library/src/main/java/com/google/android/exoplayer2/FormatHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/FormatHolder.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/FormatHolder.java rename to library/core/src/main/java/com/google/android/exoplayer2/FormatHolder.java diff --git a/library/src/main/java/com/google/android/exoplayer2/IllegalSeekPositionException.java b/library/core/src/main/java/com/google/android/exoplayer2/IllegalSeekPositionException.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/IllegalSeekPositionException.java rename to library/core/src/main/java/com/google/android/exoplayer2/IllegalSeekPositionException.java diff --git a/library/src/main/java/com/google/android/exoplayer2/LoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/LoadControl.java rename to library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java diff --git a/library/src/main/java/com/google/android/exoplayer2/ParserException.java b/library/core/src/main/java/com/google/android/exoplayer2/ParserException.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/ParserException.java rename to library/core/src/main/java/com/google/android/exoplayer2/ParserException.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java new file mode 100644 index 0000000000..90aded7660 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java @@ -0,0 +1,83 @@ +/* + * 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.google.android.exoplayer2; + +/** + * The parameters that apply to playback. + */ +public final class PlaybackParameters { + + /** + * The default playback parameters: real-time playback with no pitch modification. + */ + public static final PlaybackParameters DEFAULT = new PlaybackParameters(1f, 1f); + + /** + * The factor by which playback will be sped up. + */ + public final float speed; + + /** + * The factor by which the audio pitch will be scaled. + */ + public final float pitch; + + private final int scaledUsPerMs; + + /** + * Creates new playback parameters. + * + * @param speed The factor by which playback will be sped up. + * @param pitch The factor by which the audio pitch will be scaled. + */ + public PlaybackParameters(float speed, float pitch) { + this.speed = speed; + this.pitch = pitch; + scaledUsPerMs = Math.round(speed * 1000f); + } + + /** + * Scales the millisecond duration {@code timeMs} by the playback speed, returning the result in + * microseconds. + * + * @param timeMs The time to scale, in milliseconds. + * @return The scaled time, in microseconds. + */ + public long getSpeedAdjustedDurationUs(long timeMs) { + return timeMs * scaledUsPerMs; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + PlaybackParameters other = (PlaybackParameters) obj; + return this.speed == other.speed && this.pitch == other.pitch; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + Float.floatToRawIntBits(speed); + result = 31 * result + Float.floatToRawIntBits(pitch); + return result; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/Renderer.java b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/Renderer.java rename to library/core/src/main/java/com/google/android/exoplayer2/Renderer.java diff --git a/library/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java rename to library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java diff --git a/library/src/main/java/com/google/android/exoplayer2/RendererConfiguration.java b/library/core/src/main/java/com/google/android/exoplayer2/RendererConfiguration.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/RendererConfiguration.java rename to library/core/src/main/java/com/google/android/exoplayer2/RendererConfiguration.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/RenderersFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/RenderersFactory.java new file mode 100644 index 0000000000..728cfa387a --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/RenderersFactory.java @@ -0,0 +1,44 @@ +/* + * 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.google.android.exoplayer2; + +import android.os.Handler; +import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.metadata.MetadataRenderer; +import com.google.android.exoplayer2.text.TextRenderer; +import com.google.android.exoplayer2.video.VideoRendererEventListener; + +/** + * Builds {@link Renderer} instances for use by a {@link SimpleExoPlayer}. + */ +public interface RenderersFactory { + + /** + * Builds the {@link Renderer} instances for a {@link SimpleExoPlayer}. + * + * @param eventHandler A handler to use when invoking event listeners and outputs. + * @param videoRendererEventListener An event listener for video renderers. + * @param videoRendererEventListener An event listener for audio renderers. + * @param textRendererOutput An output for text renderers. + * @param metadataRendererOutput An output for metadata renderers. + * @return The {@link Renderer instances}. + */ + Renderer[] createRenderers(Handler eventHandler, + VideoRendererEventListener videoRendererEventListener, + AudioRendererEventListener audioRendererEventListener, + TextRenderer.Output textRendererOutput, MetadataRenderer.Output metadataRendererOutput); + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java similarity index 65% rename from library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java rename to library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 3ce4937911..28ba8cf9d7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -16,25 +16,18 @@ package com.google.android.exoplayer2; import android.annotation.TargetApi; -import android.content.Context; import android.graphics.SurfaceTexture; import android.media.MediaCodec; import android.media.PlaybackParams; import android.os.Handler; -import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import android.util.Log; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.TextureView; -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.decoder.DecoderCounters; -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.Metadata; import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.source.MediaSource; @@ -43,12 +36,7 @@ import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextRenderer; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; 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; /** @@ -90,38 +78,12 @@ public class SimpleExoPlayer implements ExoPlayer { } - /** - * Modes for using extension renderers. - */ - @Retention(RetentionPolicy.SOURCE) - @IntDef({EXTENSION_RENDERER_MODE_OFF, EXTENSION_RENDERER_MODE_ON, EXTENSION_RENDERER_MODE_PREFER}) - public @interface ExtensionRendererMode {} - /** - * Do not allow use of extension renderers. - */ - public 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. - */ - public 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. - */ - public static final int EXTENSION_RENDERER_MODE_PREFER = 2; - private static final String TAG = "SimpleExoPlayer"; - protected static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50; + + protected final Renderer[] renderers; private final ExoPlayer player; - private final Renderer[] renderers; private final ComponentListener componentListener; - private final Handler mainHandler; private final int videoRendererCount; private final int audioRendererCount; @@ -145,19 +107,12 @@ public class SimpleExoPlayer implements ExoPlayer { @C.StreamType private int audioStreamType; private float audioVolume; - private PlaybackParamsHolder playbackParamsHolder; - protected SimpleExoPlayer(Context context, TrackSelector trackSelector, LoadControl loadControl, - DrmSessionManager drmSessionManager, - @ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs) { - mainHandler = new Handler(); + protected SimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector, + LoadControl loadControl) { componentListener = new ComponentListener(); - - // Build the renderers. - ArrayList renderersList = new ArrayList<>(); - buildRenderers(context, mainHandler, drmSessionManager, extensionRendererMode, - allowedVideoJoiningTimeMs, renderersList); - renderers = renderersList.toArray(new Renderer[renderersList.size()]); + renderers = renderersFactory.createRenderers(new Handler(), componentListener, + componentListener, componentListener, componentListener); // Obtain counts of video and audio renderers. int videoRendererCount = 0; @@ -239,6 +194,18 @@ public class SimpleExoPlayer implements ExoPlayer { setVideoSurfaceInternal(surface, false); } + /** + * Clears the {@link Surface} onto which video is being rendered if it matches the one passed. + * Else does nothing. + * + * @param surface The surface to clear. + */ + public void clearVideoSurface(Surface surface) { + if (surface != null && surface == this.surface) { + setVideoSurface(null); + } + } + /** * Sets the {@link SurfaceHolder} that holds the {@link Surface} onto which video will be * rendered. The player will track the lifecycle of the surface automatically. @@ -256,6 +223,18 @@ public class SimpleExoPlayer implements ExoPlayer { } } + /** + * Clears the {@link SurfaceHolder} that holds the {@link Surface} onto which video is being + * rendered if it matches the one passed. Else does nothing. + * + * @param surfaceHolder The surface holder to clear. + */ + public void clearVideoSurfaceHolder(SurfaceHolder surfaceHolder) { + if (surfaceHolder != null && surfaceHolder == this.surfaceHolder) { + setVideoSurfaceHolder(null); + } + } + /** * Sets the {@link SurfaceView} onto which video will be rendered. The player will track the * lifecycle of the surface automatically. @@ -263,7 +242,17 @@ public class SimpleExoPlayer implements ExoPlayer { * @param surfaceView The surface view. */ public void setVideoSurfaceView(SurfaceView surfaceView) { - setVideoSurfaceHolder(surfaceView.getHolder()); + setVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder()); + } + + /** + * Clears the {@link SurfaceView} onto which video is being rendered if it matches the one passed. + * Else does nothing. + * + * @param surfaceView The texture view to clear. + */ + public void clearVideoSurfaceView(SurfaceView surfaceView) { + clearVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder()); } /** @@ -287,6 +276,18 @@ public class SimpleExoPlayer implements ExoPlayer { } } + /** + * Clears the {@link TextureView} onto which video is being rendered if it matches the one passed. + * Else does nothing. + * + * @param textureView The texture view to clear. + */ + public void clearVideoTextureView(TextureView textureView) { + if (textureView != null && textureView == this.textureView) { + setVideoTextureView(null); + } + } + /** * Sets the stream type for audio playback (see {@link C.StreamType} and * {@link android.media.AudioTrack#AudioTrack(int, int, int, int, int, int)}). If the stream type @@ -344,37 +345,20 @@ public class SimpleExoPlayer implements ExoPlayer { /** * Sets the {@link PlaybackParams} governing audio playback. * + * @deprecated Use {@link #setPlaybackParameters(PlaybackParameters)}. * @param params The {@link PlaybackParams}, or null to clear any previously set parameters. */ + @Deprecated @TargetApi(23) - public void setPlaybackParams(PlaybackParams params) { + public void setPlaybackParams(@Nullable PlaybackParams params) { + PlaybackParameters playbackParameters; if (params != null) { - // The audio renderers will call this on the playback thread to ensure they can query - // parameters without failure. We do the same up front, which is redundant except that it - // ensures an immediate call to getPlaybackParams will retrieve the instance with defaults - // allowed, rather than this change becoming visible sometime later once the audio renderers - // receive the parameters. params.allowDefaults(); - playbackParamsHolder = new PlaybackParamsHolder(params); + playbackParameters = new PlaybackParameters(params.getSpeed(), params.getPitch()); } else { - playbackParamsHolder = null; + playbackParameters = null; } - ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount]; - int count = 0; - for (Renderer renderer : renderers) { - if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { - messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_PLAYBACK_PARAMS, params); - } - } - player.sendMessages(messages); - } - - /** - * Returns the {@link PlaybackParams} governing audio playback, or null if not set. - */ - @TargetApi(23) - public PlaybackParams getPlaybackParams() { - return playbackParamsHolder == null ? null : playbackParamsHolder.params; + setPlaybackParameters(playbackParameters); } /** @@ -421,6 +405,57 @@ public class SimpleExoPlayer implements ExoPlayer { videoListener = listener; } + /** + * Clears the listener receiving video events if it matches the one passed. Else does nothing. + * + * @param listener The listener to clear. + */ + public void clearVideoListener(VideoListener listener) { + if (videoListener == listener) { + videoListener = null; + } + } + + /** + * Sets an output to receive text events. + * + * @param output The output. + */ + public void setTextOutput(TextRenderer.Output output) { + textOutput = output; + } + + /** + * Clears the output receiving text events if it matches the one passed. Else does nothing. + * + * @param output The output to clear. + */ + public void clearTextOutput(TextRenderer.Output output) { + if (textOutput == output) { + textOutput = null; + } + } + + /** + * Sets a listener to receive metadata events. + * + * @param output The output. + */ + public void setMetadataOutput(MetadataRenderer.Output output) { + metadataOutput = output; + } + + /** + * Clears the output receiving metadata events if it matches the one passed. Else does nothing. + * + * @param output The output to clear. + */ + public void clearMetadataOutput(MetadataRenderer.Output output) { + if (metadataOutput == output) { + metadataOutput = null; + } + } + /** * Sets a listener to receive debug events from the video renderer. * @@ -439,24 +474,6 @@ public class SimpleExoPlayer implements ExoPlayer { audioDebugListener = listener; } - /** - * Sets an output to receive text events. - * - * @param output The output. - */ - public void setTextOutput(TextRenderer.Output output) { - textOutput = output; - } - - /** - * Sets a listener to receive metadata events. - * - * @param output The output. - */ - public void setMetadataOutput(MetadataRenderer.Output output) { - metadataOutput = output; - } - // ExoPlayer implementation @Override @@ -519,6 +536,16 @@ public class SimpleExoPlayer implements ExoPlayer { player.seekTo(windowIndex, positionMs); } + @Override + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + player.setPlaybackParameters(playbackParameters); + } + + @Override + public PlaybackParameters getPlaybackParameters() { + return player.getPlaybackParameters(); + } + @Override public void stop() { player.stop(); @@ -616,190 +643,6 @@ public class SimpleExoPlayer implements ExoPlayer { return player.isCurrentWindowSeekable(); } - // Renderer building. - - private void buildRenderers(Context context, Handler mainHandler, - DrmSessionManager drmSessionManager, - @ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs, - ArrayList out) { - buildVideoRenderers(context, mainHandler, drmSessionManager, extensionRendererMode, - componentListener, allowedVideoJoiningTimeMs, out); - buildAudioRenderers(context, mainHandler, drmSessionManager, extensionRendererMode, - componentListener, buildAudioProcessors(), out); - buildTextRenderers(context, mainHandler, extensionRendererMode, componentListener, out); - buildMetadataRenderers(context, mainHandler, extensionRendererMode, componentListener, out); - buildMiscellaneousRenderers(context, mainHandler, extensionRendererMode, out); - } - - /** - * Builds video renderers for use by the player. - * - * @param context The {@link Context} associated with the player. - * @param mainHandler A handler associated with the main thread's looper. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will - * not be used for DRM protected playbacks. - * @param extensionRendererMode The extension renderer mode. - * @param eventListener An event listener. - * @param allowedVideoJoiningTimeMs The maximum duration in milliseconds for which video renderers - * can attempt to seamlessly join an ongoing playback. - * @param out An array to which the built renderers should be appended. - */ - protected void buildVideoRenderers(Context context, Handler mainHandler, - DrmSessionManager drmSessionManager, - @ExtensionRendererMode int extensionRendererMode, VideoRendererEventListener eventListener, - long allowedVideoJoiningTimeMs, ArrayList out) { - out.add(new MediaCodecVideoRenderer(context, MediaCodecSelector.DEFAULT, - allowedVideoJoiningTimeMs, drmSessionManager, false, mainHandler, eventListener, - MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)); - - if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { - return; - } - int extensionRendererIndex = out.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(true, allowedVideoJoiningTimeMs, - mainHandler, componentListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY); - out.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 RuntimeException(e); - } - } - - /** - * Builds audio renderers for use by the player. - * - * @param context The {@link Context} associated with the player. - * @param mainHandler A handler associated with the main thread's looper. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will - * not be used for DRM protected playbacks. - * @param extensionRendererMode The extension renderer mode. - * @param eventListener An event listener. - * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio buffers - * before output. May be empty. - * @param out An array to which the built renderers should be appended. - */ - protected void buildAudioRenderers(Context context, Handler mainHandler, - DrmSessionManager drmSessionManager, - @ExtensionRendererMode int extensionRendererMode, AudioRendererEventListener eventListener, - AudioProcessor[] audioProcessors, ArrayList out) { - out.add(new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT, drmSessionManager, true, - mainHandler, eventListener, AudioCapabilities.getCapabilities(context), audioProcessors)); - - if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { - return; - } - int extensionRendererIndex = out.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(mainHandler, componentListener, - audioProcessors); - out.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 RuntimeException(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(mainHandler, componentListener, - audioProcessors); - out.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 RuntimeException(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(mainHandler, componentListener, - audioProcessors); - out.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 RuntimeException(e); - } - } - - /** - * Builds text renderers for use by the player. - * - * @param context The {@link Context} associated with the player. - * @param mainHandler A handler associated with the main thread's looper. - * @param extensionRendererMode The extension renderer mode. - * @param output An output for the renderers. - * @param out An array to which the built renderers should be appended. - */ - protected void buildTextRenderers(Context context, Handler mainHandler, - @ExtensionRendererMode int extensionRendererMode, TextRenderer.Output output, - ArrayList out) { - out.add(new TextRenderer(output, mainHandler.getLooper())); - } - - /** - * Builds metadata renderers for use by the player. - * - * @param context The {@link Context} associated with the player. - * @param mainHandler A handler associated with the main thread's looper. - * @param extensionRendererMode The extension renderer mode. - * @param output An output for the renderers. - * @param out An array to which the built renderers should be appended. - */ - protected void buildMetadataRenderers(Context context, Handler mainHandler, - @ExtensionRendererMode int extensionRendererMode, MetadataRenderer.Output output, - ArrayList out) { - out.add(new MetadataRenderer(output, mainHandler.getLooper())); - } - - /** - * Builds any miscellaneous renderers used by the player. - * - * @param context The {@link Context} associated with the player. - * @param mainHandler A handler associated with the main thread's looper. - * @param extensionRendererMode The extension renderer mode. - * @param out An array to which the built renderers should be appended. - */ - protected void buildMiscellaneousRenderers(Context context, Handler mainHandler, - @ExtensionRendererMode int extensionRendererMode, ArrayList out) { - // Do nothing. - } - - /** - * Builds an array of {@link AudioProcessor}s that will process PCM audio before output. - */ - protected AudioProcessor[] buildAudioProcessors() { - return new AudioProcessor[0]; - } - // Internal methods. private void removeSurfaceCallbacks() { @@ -1024,15 +867,4 @@ public class SimpleExoPlayer implements ExoPlayer { } - @TargetApi(23) - private static final class PlaybackParamsHolder { - - public final PlaybackParams params; - - public PlaybackParamsHolder(PlaybackParams params) { - this.params = params; - } - - } - } diff --git a/library/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java similarity index 99% rename from library/src/main/java/com/google/android/exoplayer2/Timeline.java rename to library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index 333dd25cbe..eb3966ae4d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -383,18 +383,24 @@ public abstract class Timeline { */ public long durationUs; + /** + * Whether this period contains an ad. + */ + public boolean isAd; + private long positionInWindowUs; /** * Sets the data held by this period. */ public Period set(Object id, Object uid, int windowIndex, long durationUs, - long positionInWindowUs) { + long positionInWindowUs, boolean isAd) { this.id = id; this.uid = uid; this.windowIndex = windowIndex; this.durationUs = durationUs; this.positionInWindowUs = positionInWindowUs; + this.isAd = isAd; return this; } diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java similarity index 68% rename from library/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java rename to library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java index adb3d5999b..4b64ffb030 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java @@ -28,6 +28,44 @@ import java.nio.ByteBuffer; */ public final class Ac3Util { + /** + * Holds sample format information as presented by a syncframe header. + */ + public static final class Ac3SyncFrameInfo { + + /** + * The sample mime type of the bitstream. One of {@link MimeTypes#AUDIO_AC3} and + * {@link MimeTypes#AUDIO_E_AC3}. + */ + public final String mimeType; + /** + * The audio sampling rate in Hz. + */ + public final int sampleRate; + /** + * The number of audio channels + */ + public final int channelCount; + /** + * The size of the frame. + */ + public final int frameSize; + /** + * Number of audio samples in the frame. + */ + public final int sampleCount; + + private Ac3SyncFrameInfo(String mimeType, int channelCount, int sampleRate, int frameSize, + int sampleCount) { + this.mimeType = mimeType; + this.channelCount = channelCount; + this.sampleRate = sampleRate; + this.frameSize = frameSize; + this.sampleCount = sampleCount; + } + + } + /** * The number of new samples per (E-)AC-3 audio block. */ @@ -114,62 +152,61 @@ public final class Ac3Util { } /** - * Returns the AC-3 format given {@code data} containing a syncframe. The reading position of - * {@code data} will be modified. + * Returns (E-)AC-3 format information given {@code data} containing a syncframe. The reading + * position of {@code data} will be modified. * * @param data The data to parse, positioned at the start of the syncframe. - * @param trackId The track identifier to set on the format, or null. - * @param language The language to set on the format. - * @param drmInitData {@link DrmInitData} to be included in the format. - * @return The AC-3 format parsed from data in the header. + * @return The (E-)AC-3 format data parsed from the header. */ - public static Format parseAc3SyncframeFormat(ParsableBitArray data, String trackId, - String language, DrmInitData drmInitData) { - data.skipBits(16 + 16); // syncword, crc1 - int fscod = data.readBits(2); - data.skipBits(6 + 5 + 3); // frmsizecod, bsid, bsmod - int acmod = data.readBits(3); - if ((acmod & 0x01) != 0 && acmod != 1) { - data.skipBits(2); // cmixlev - } - if ((acmod & 0x04) != 0) { - data.skipBits(2); // surmixlev - } - if (acmod == 2) { - data.skipBits(2); // dsurmod - } - boolean lfeon = data.readBit(); - return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_AC3, null, Format.NO_VALUE, - Format.NO_VALUE, CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0), - SAMPLE_RATE_BY_FSCOD[fscod], null, drmInitData, 0, language); - } - - /** - * Returns the E-AC-3 format given {@code data} containing a syncframe. The reading position of - * {@code data} will be modified. - * - * @param data The data to parse, positioned at the start of the syncframe. - * @param trackId The track identifier to set on the format, or null. - * @param language The language to set on the format. - * @param drmInitData {@link DrmInitData} to be included in the format. - * @return The E-AC-3 format parsed from data in the header. - */ - public static Format parseEac3SyncframeFormat(ParsableBitArray data, String trackId, - String language, DrmInitData drmInitData) { - data.skipBits(16 + 2 + 3 + 11); // syncword, strmtype, substreamid, frmsiz + public static Ac3SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) { + int initialPosition = data.getPosition(); + data.skipBits(40); + boolean isEac3 = data.readBits(5) == 16; + data.setPosition(initialPosition); + String mimeType; int sampleRate; - int fscod = data.readBits(2); - if (fscod == 3) { - sampleRate = SAMPLE_RATE_BY_FSCOD2[data.readBits(2)]; - } else { - data.skipBits(2); // numblkscod + int acmod; + int frameSize; + int sampleCount; + if (isEac3) { + mimeType = MimeTypes.AUDIO_E_AC3; + data.skipBits(16 + 2 + 3); // syncword, strmtype, substreamid + frameSize = (data.readBits(11) + 1) * 2; + int fscod = data.readBits(2); + int audioBlocks; + if (fscod == 3) { + sampleRate = SAMPLE_RATE_BY_FSCOD2[data.readBits(2)]; + audioBlocks = 6; + } else { + int numblkscod = data.readBits(2); + audioBlocks = BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[numblkscod]; + sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; + } + sampleCount = AUDIO_SAMPLES_PER_AUDIO_BLOCK * audioBlocks; + acmod = data.readBits(3); + } else /* is AC-3 */ { + mimeType = MimeTypes.AUDIO_AC3; + data.skipBits(16 + 16); // syncword, crc1 + int fscod = data.readBits(2); + int frmsizecod = data.readBits(6); + frameSize = getAc3SyncframeSize(fscod, frmsizecod); + data.skipBits(5 + 3); // bsid, bsmod + acmod = data.readBits(3); + if ((acmod & 0x01) != 0 && acmod != 1) { + data.skipBits(2); // cmixlev + } + if ((acmod & 0x04) != 0) { + data.skipBits(2); // surmixlev + } + if (acmod == 2) { + data.skipBits(2); // dsurmod + } sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; + sampleCount = AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT; } - int acmod = data.readBits(3); boolean lfeon = data.readBit(); - return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_E_AC3, null, Format.NO_VALUE, - Format.NO_VALUE, CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0), sampleRate, null, - drmInitData, 0, language); + int channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0); + return new Ac3SyncFrameInfo(mimeType, channelCount, sampleRate, frameSize, sampleCount); } /** @@ -187,16 +224,6 @@ public final class Ac3Util { return getAc3SyncframeSize(fscod, frmsizecod); } - /** - * Returns the size in bytes of the given E-AC-3 syncframe. - * - * @param data The syncframe to parse. - * @return The syncframe size in bytes. - */ - public static int parseEAc3SyncframeSize(byte[] data) { - return 2 * (((data[2] & 0x07) << 8) + (data[3] & 0xFF) + 1); // frmsiz - } - /** * Returns the number of audio samples in an AC-3 syncframe. */ @@ -205,22 +232,10 @@ public final class Ac3Util { } /** - * Returns the number of audio samples represented by the given E-AC-3 syncframe. + * Reads the number of audio samples represented by the given E-AC-3 syncframe. The buffer's + * position is not modified. * - * @param data The syncframe to parse. - * @return The number of audio samples represented by the syncframe. - */ - public static int parseEAc3SyncframeAudioSampleCount(byte[] data) { - // See ETSI TS 102 366 subsection E.1.2.2. - return AUDIO_SAMPLES_PER_AUDIO_BLOCK * (((data[4] & 0xC0) >> 6) == 0x03 ? 6 // fscod - : BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[(data[4] & 0x30) >> 4]); - } - - /** - * Like {@link #parseEAc3SyncframeAudioSampleCount(byte[])} but reads from a {@link ByteBuffer}. - * The buffer's position is not modified. - * - * @param buffer The {@link ByteBuffer} from which to read. + * @param buffer The {@link ByteBuffer} from which to read the syncframe. * @return The number of audio samples represented by the syncframe. */ public static int parseEAc3SyncframeAudioSampleCount(ByteBuffer buffer) { diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilities.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilities.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilities.java rename to library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilities.java diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilitiesReceiver.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilitiesReceiver.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilitiesReceiver.java rename to library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilitiesReceiver.java diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/AudioDecoderException.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioDecoderException.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/audio/AudioDecoderException.java rename to library/core/src/main/java/com/google/android/exoplayer2/audio/AudioDecoderException.java diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java similarity index 98% rename from library/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java rename to library/core/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java index 2e0d1f98d9..eced040812 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java @@ -116,8 +116,8 @@ public interface AudioProcessor { void flush(); /** - * Releases any resources associated with this instance. + * Resets the processor to its initial state. */ - void release(); + void reset(); } diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java rename to library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java similarity index 87% rename from library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java rename to library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java index 3b8a1b8f39..44a96373f3 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java @@ -20,11 +20,11 @@ import android.annotation.TargetApi; import android.media.AudioAttributes; import android.media.AudioFormat; import android.media.AudioTimestamp; -import android.media.PlaybackParams; import android.os.ConditionVariable; import android.os.SystemClock; import android.util.Log; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -32,6 +32,7 @@ import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; +import java.util.LinkedList; /** * Plays audio data. The implementation delegates to an {@link android.media.AudioTrack} and handles @@ -170,7 +171,7 @@ public final class AudioTrack { } /** - * Returned by {@link #getCurrentPositionUs} when the position is not set. + * Returned by {@link #getCurrentPositionUs(boolean)} when the position is not set. */ public static final long CURRENT_POSITION_NOT_SET = Long.MIN_VALUE; @@ -251,6 +252,13 @@ public final class AudioTrack { private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000; private static final int MIN_TIMESTAMP_SAMPLE_INTERVAL_US = 500000; + /** + * The minimum number of output bytes from {@link #sonicAudioProcessor} at which the speedup is + * calculated using the input/output byte counts from the processor, rather than using the + * current playback parameters speed. + */ + private static final int SONIC_MIN_BYTES_FOR_SPEEDUP = 1024; + /** * Whether to enable a workaround for an issue where an audio effect does not keep its session * active across releasing/initializing a new audio track, on platform builds where @@ -271,11 +279,13 @@ public final class AudioTrack { private final AudioCapabilities audioCapabilities; private final ChannelMappingAudioProcessor channelMappingAudioProcessor; + private final SonicAudioProcessor sonicAudioProcessor; private final AudioProcessor[] availableAudioProcessors; private final Listener listener; private final ConditionVariable releasingConditionVariable; private final long[] playheadOffsets; private final AudioTrackUtil audioTrackUtil; + private final LinkedList playbackParametersCheckpoints; /** * Used to keep the audio session active on pre-V21 builds (see {@link #initialize()}). @@ -295,6 +305,11 @@ public final class AudioTrack { private int bufferSize; private long bufferSizeUs; + private PlaybackParameters drainingPlaybackParameters; + private PlaybackParameters playbackParameters; + private long playbackParametersOffsetUs; + private long playbackParametersPositionUs; + private ByteBuffer avSyncHeader; private int bytesUntilNextAvSync; @@ -344,11 +359,6 @@ public final class AudioTrack { public AudioTrack(AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors, Listener listener) { this.audioCapabilities = audioCapabilities; - channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); - availableAudioProcessors = new AudioProcessor[audioProcessors.length + 2]; - availableAudioProcessors[0] = new ResamplingAudioProcessor(); - availableAudioProcessors[1] = channelMappingAudioProcessor; - System.arraycopy(audioProcessors, 0, availableAudioProcessors, 2, audioProcessors.length); this.listener = listener; releasingConditionVariable = new ConditionVariable(true); if (Util.SDK_INT >= 18) { @@ -359,21 +369,28 @@ public final class AudioTrack { // There's no guarantee this method exists. Do nothing. } } - if (Util.SDK_INT >= 23) { - audioTrackUtil = new AudioTrackUtilV23(); - } else if (Util.SDK_INT >= 19) { + if (Util.SDK_INT >= 19) { audioTrackUtil = new AudioTrackUtilV19(); } else { audioTrackUtil = new AudioTrackUtil(); } + channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); + sonicAudioProcessor = new SonicAudioProcessor(); + availableAudioProcessors = new AudioProcessor[3 + audioProcessors.length]; + availableAudioProcessors[0] = new ResamplingAudioProcessor(); + availableAudioProcessors[1] = channelMappingAudioProcessor; + System.arraycopy(audioProcessors, 0, availableAudioProcessors, 2, audioProcessors.length); + availableAudioProcessors[2 + audioProcessors.length] = sonicAudioProcessor; playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT]; volume = 1.0f; startMediaTimeState = START_NOT_SET; streamType = C.STREAM_TYPE_DEFAULT; audioSessionId = C.AUDIO_SESSION_ID_UNSET; + playbackParameters = PlaybackParameters.DEFAULT; drainingAudioProcessorIndex = C.INDEX_UNSET; this.audioProcessors = new AudioProcessor[0]; outputBuffers = new ByteBuffer[0]; + playbackParametersCheckpoints = new LinkedList<>(); } /** @@ -408,33 +425,29 @@ public final class AudioTrack { } long systemClockUs = System.nanoTime() / 1000; - long currentPositionUs; + long positionUs; if (audioTimestampSet) { - // How long ago in the past the audio timestamp is (negative if it's in the future). - long presentationDiff = systemClockUs - (audioTrackUtil.getTimestampNanoTime() / 1000); - // Fixes such difference if the playback speed is not real time speed. - long actualSpeedPresentationDiff = (long) (presentationDiff - * audioTrackUtil.getPlaybackSpeed()); - long framesDiff = durationUsToFrames(actualSpeedPresentationDiff); - // The position of the frame that's currently being presented. - long currentFramePosition = audioTrackUtil.getTimestampFramePosition() + framesDiff; - currentPositionUs = framesToDurationUs(currentFramePosition) + startMediaTimeUs; + // Calculate the speed-adjusted position using the timestamp (which may be in the future). + long elapsedSinceTimestampUs = systemClockUs - (audioTrackUtil.getTimestampNanoTime() / 1000); + long elapsedSinceTimestampFrames = durationUsToFrames(elapsedSinceTimestampUs); + long elapsedFrames = audioTrackUtil.getTimestampFramePosition() + elapsedSinceTimestampFrames; + positionUs = framesToDurationUs(elapsedFrames); } else { if (playheadOffsetCount == 0) { // The AudioTrack has started, but we don't have any samples to compute a smoothed position. - currentPositionUs = audioTrackUtil.getPlaybackHeadPositionUs() + startMediaTimeUs; + positionUs = audioTrackUtil.getPositionUs(); } else { // getPlayheadPositionUs() only has a granularity of ~20 ms, so we base the position off the // system clock (and a smoothed offset between it and the playhead position) so as to // prevent jitter in the reported positions. - currentPositionUs = systemClockUs + smoothedPlayheadOffsetUs + startMediaTimeUs; + positionUs = systemClockUs + smoothedPlayheadOffsetUs; } if (!sourceEnded) { - currentPositionUs -= latencyUs; + positionUs -= latencyUs; } } - return currentPositionUs; + return startMediaTimeUs + applySpeedup(positionUs); } /** @@ -481,10 +494,7 @@ public final class AudioTrack { boolean flush = false; if (!passthrough) { pcmFrameSize = Util.getPcmFrameSize(pcmEncoding, channelCount); - - // Reconfigure the audio processors. channelMappingAudioProcessor.setChannelMap(outputChannels); - ArrayList newAudioProcessors = new ArrayList<>(); for (AudioProcessor audioProcessor : availableAudioProcessors) { try { flush |= audioProcessor.configure(sampleRate, channelCount, encoding); @@ -492,23 +502,12 @@ public final class AudioTrack { throw new ConfigurationException(e); } if (audioProcessor.isActive()) { - newAudioProcessors.add(audioProcessor); channelCount = audioProcessor.getOutputChannelCount(); encoding = audioProcessor.getOutputEncoding(); - } else { - audioProcessor.flush(); } } - if (flush) { - int count = newAudioProcessors.size(); - audioProcessors = newAudioProcessors.toArray(new AudioProcessor[count]); - outputBuffers = new ByteBuffer[count]; - for (int i = 0; i < count; i++) { - AudioProcessor audioProcessor = audioProcessors[i]; - audioProcessor.flush(); - outputBuffers[i] = audioProcessor.getOutput(); - } + resetAudioProcessors(); } } @@ -603,6 +602,28 @@ public final class AudioTrack { : multipliedBufferSize; } bufferSizeUs = passthrough ? C.TIME_UNSET : framesToDurationUs(bufferSize / outputPcmFrameSize); + + // The old playback parameters may no longer be applicable so try to reset them now. + setPlaybackParameters(playbackParameters); + } + + private void resetAudioProcessors() { + ArrayList newAudioProcessors = new ArrayList<>(); + for (AudioProcessor audioProcessor : availableAudioProcessors) { + if (audioProcessor.isActive()) { + newAudioProcessors.add(audioProcessor); + } else { + audioProcessor.flush(); + } + } + int count = newAudioProcessors.size(); + audioProcessors = newAudioProcessors.toArray(new AudioProcessor[count]); + outputBuffers = new ByteBuffer[count]; + for (int i = 0; i < count; i++) { + AudioProcessor audioProcessor = audioProcessors[i]; + audioProcessor.flush(); + outputBuffers[i] = audioProcessor.getOutput(); + } } private void initialize() throws InitializationException { @@ -741,6 +762,21 @@ public final class AudioTrack { framesPerEncodedSample = getFramesPerEncodedSample(outputEncoding, buffer); } + if (drainingPlaybackParameters != null) { + if (!drainAudioProcessorsToEndOfStream()) { + // Don't process any more input until draining completes. + return false; + } + // Store the position and corresponding media time from which the parameters will apply. + playbackParametersCheckpoints.add(new PlaybackParametersCheckpoint( + drainingPlaybackParameters, Math.max(0, presentationTimeUs), + framesToDurationUs(getWrittenFrames()))); + drainingPlaybackParameters = null; + // The audio processors have drained, so flush them. This will cause any active speed + // adjustment audio processor to start producing audio with the new parameters. + resetAudioProcessors(); + } + if (startMediaTimeState == START_NOT_SET) { startMediaTimeUs = Math.max(0, presentationTimeUs); startMediaTimeState = START_IN_SYNC; @@ -889,7 +925,15 @@ public final class AudioTrack { return; } - // Drain the audio processors. + if (drainAudioProcessorsToEndOfStream()) { + // The audio processors have drained, so drain the underlying audio track. + audioTrackUtil.handleEndOfStream(getWrittenFrames()); + bytesUntilNextAvSync = 0; + handledEndOfStream = true; + } + } + + private boolean drainAudioProcessorsToEndOfStream() throws WriteException { boolean audioProcessorNeedsEndOfStream = false; if (drainingAudioProcessorIndex == C.INDEX_UNSET) { drainingAudioProcessorIndex = passthrough ? audioProcessors.length : 0; @@ -902,7 +946,7 @@ public final class AudioTrack { } processBuffers(C.TIME_UNSET); if (!audioProcessor.isEnded()) { - return; + return false; } audioProcessorNeedsEndOfStream = true; drainingAudioProcessorIndex++; @@ -912,14 +956,11 @@ public final class AudioTrack { if (outputBuffer != null) { writeBuffer(outputBuffer, C.TIME_UNSET); if (outputBuffer != null) { - return; + return false; } } - - // Drain the track. - audioTrackUtil.handleEndOfStream(getWrittenFrames()); - bytesUntilNextAvSync = 0; - handledEndOfStream = true; + drainingAudioProcessorIndex = C.INDEX_UNSET; + return true; } /** @@ -940,15 +981,43 @@ public final class AudioTrack { } /** - * Sets the playback parameters. Only available for {@link Util#SDK_INT} >= 23 + * Attempts to set the playback parameters and returns the active playback parameters, which may + * differ from those passed in. * - * @param playbackParams The playback parameters to be used by the - * {@link android.media.AudioTrack}. - * @throws UnsupportedOperationException if the Playback Parameters are not supported. That is, - * {@link Util#SDK_INT} < 23. + * @param playbackParameters The new playback parameters to attempt to set. + * @return The active playback parameters. */ - public void setPlaybackParams(PlaybackParams playbackParams) { - audioTrackUtil.setPlaybackParams(playbackParams); + public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { + if (passthrough) { + // The playback parameters are always the default in passthrough mode. + this.playbackParameters = PlaybackParameters.DEFAULT; + return this.playbackParameters; + } + playbackParameters = new PlaybackParameters( + sonicAudioProcessor.setSpeed(playbackParameters.speed), + sonicAudioProcessor.setPitch(playbackParameters.pitch)); + PlaybackParameters lastSetPlaybackParameters = + drainingPlaybackParameters != null ? drainingPlaybackParameters + : !playbackParametersCheckpoints.isEmpty() + ? playbackParametersCheckpoints.getLast().playbackParameters + : this.playbackParameters; + if (!playbackParameters.equals(lastSetPlaybackParameters)) { + if (isInitialized()) { + // Drain the audio processors so we can determine the frame position at which the new + // parameters apply. + drainingPlaybackParameters = playbackParameters; + } else { + this.playbackParameters = playbackParameters; + } + } + return this.playbackParameters; + } + + /** + * Gets the {@link PlaybackParameters}. + */ + public PlaybackParameters getPlaybackParameters() { + return playbackParameters; } /** @@ -1065,6 +1134,15 @@ public final class AudioTrack { writtenPcmBytes = 0; writtenEncodedFrames = 0; framesPerEncodedSample = 0; + if (drainingPlaybackParameters != null) { + playbackParameters = drainingPlaybackParameters; + drainingPlaybackParameters = null; + } else if (!playbackParametersCheckpoints.isEmpty()) { + playbackParameters = playbackParametersCheckpoints.getLast().playbackParameters; + } + playbackParametersCheckpoints.clear(); + playbackParametersOffsetUs = 0; + playbackParametersPositionUs = 0; inputBuffer = null; outputBuffer = null; for (int i = 0; i < audioProcessors.length; i++) { @@ -1109,7 +1187,7 @@ public final class AudioTrack { reset(); releaseKeepSessionIdAudioTrack(); for (AudioProcessor audioProcessor : availableAudioProcessors) { - audioProcessor.release(); + audioProcessor.reset(); } audioSessionId = C.AUDIO_SESSION_ID_UNSET; playing = false; @@ -1141,11 +1219,41 @@ public final class AudioTrack { return isInitialized() && startMediaTimeState != START_NOT_SET; } + /** + * Returns the underlying audio track {@code positionUs} with any applicable speedup applied. + */ + private long applySpeedup(long positionUs) { + while (!playbackParametersCheckpoints.isEmpty() + && positionUs >= playbackParametersCheckpoints.getFirst().positionUs) { + // We are playing (or about to play) media with the new playback parameters, so update them. + PlaybackParametersCheckpoint checkpoint = playbackParametersCheckpoints.remove(); + playbackParameters = checkpoint.playbackParameters; + playbackParametersPositionUs = checkpoint.positionUs; + playbackParametersOffsetUs = checkpoint.mediaTimeUs - startMediaTimeUs; + } + + if (playbackParameters.speed == 1f) { + return positionUs + playbackParametersOffsetUs - playbackParametersPositionUs; + } + + if (playbackParametersCheckpoints.isEmpty() + && sonicAudioProcessor.getOutputByteCount() >= SONIC_MIN_BYTES_FOR_SPEEDUP) { + return playbackParametersOffsetUs + + Util.scaleLargeTimestamp(positionUs - playbackParametersPositionUs, + sonicAudioProcessor.getInputByteCount(), sonicAudioProcessor.getOutputByteCount()); + } + + // We are playing drained data at a previous playback speed, or don't have enough bytes to + // calculate an accurate speedup, so fall back to multiplying by the speed. + return playbackParametersOffsetUs + + (long) ((double) playbackParameters.speed * (positionUs - playbackParametersPositionUs)); + } + /** * Updates the audio track latency and playback position parameters. */ private void maybeSampleSyncParams() { - long playbackPositionUs = audioTrackUtil.getPlaybackHeadPositionUs(); + long playbackPositionUs = audioTrackUtil.getPositionUs(); if (playbackPositionUs == 0) { // The AudioTrack hasn't output anything yet. return; @@ -1441,15 +1549,15 @@ public final class AudioTrack { /** * Stops the audio track in a way that ensures media written to it is played out in full, and - * that {@link #getPlaybackHeadPosition()} and {@link #getPlaybackHeadPositionUs()} continue to - * increment as the remaining media is played out. + * that {@link #getPlaybackHeadPosition()} and {@link #getPositionUs()} continue to increment as + * the remaining media is played out. * - * @param submittedFrames The total number of frames that have been submitted. + * @param writtenFrames The total number of frames that have been written. */ - public void handleEndOfStream(long submittedFrames) { + public void handleEndOfStream(long writtenFrames) { stopPlaybackHeadPosition = getPlaybackHeadPosition(); stopTimestampUs = SystemClock.elapsedRealtime() * 1000; - endPlaybackHeadPosition = submittedFrames; + endPlaybackHeadPosition = writtenFrames; audioTrack.stop(); } @@ -1471,8 +1579,7 @@ public final class AudioTrack { * returns the playback head position as a long that will only wrap around if the value exceeds * {@link Long#MAX_VALUE} (which in practice will never happen). * - * @return {@link android.media.AudioTrack#getPlaybackHeadPosition()} of {@link #audioTrack} - * expressed as a long. + * @return The playback head position, in frames. */ public long getPlaybackHeadPosition() { if (stopTimestampUs != C.TIME_UNSET) { @@ -1507,9 +1614,9 @@ public final class AudioTrack { } /** - * Returns {@link #getPlaybackHeadPosition()} expressed as microseconds. + * Returns the duration of played media since reconfiguration, in microseconds. */ - public long getPlaybackHeadPositionUs() { + public long getPositionUs() { return (getPlaybackHeadPosition() * C.MICROS_PER_SECOND) / sampleRate; } @@ -1553,28 +1660,6 @@ public final class AudioTrack { throw new UnsupportedOperationException(); } - /** - * Sets the Playback Parameters to be used by the underlying {@link android.media.AudioTrack}. - * - * @param playbackParams The playback parameters to be used by the - * {@link android.media.AudioTrack}. - * @throws UnsupportedOperationException If Playback Parameters are not supported - * (i.e. {@link Util#SDK_INT} < 23). - */ - public void setPlaybackParams(PlaybackParams playbackParams) { - throw new UnsupportedOperationException(); - } - - /** - * Returns the configured playback speed according to the used Playback Parameters. If these are - * not supported, 1.0f(normal speed) is returned. - * - * @return The speed factor used by the underlying {@link android.media.AudioTrack}. - */ - public float getPlaybackSpeed() { - return 1.0f; - } - } @TargetApi(19) @@ -1626,41 +1711,20 @@ public final class AudioTrack { } - @TargetApi(23) - private static class AudioTrackUtilV23 extends AudioTrackUtilV19 { + /** + * Stores playback parameters with the position and media time at which they apply. + */ + private static final class PlaybackParametersCheckpoint { - private PlaybackParams playbackParams; - private float playbackSpeed; + private final PlaybackParameters playbackParameters; + private final long mediaTimeUs; + private final long positionUs; - public AudioTrackUtilV23() { - playbackSpeed = 1.0f; - } - - @Override - public void reconfigure(android.media.AudioTrack audioTrack, - boolean needsPassthroughWorkaround) { - super.reconfigure(audioTrack, needsPassthroughWorkaround); - maybeApplyPlaybackParams(); - } - - @Override - public void setPlaybackParams(PlaybackParams playbackParams) { - playbackParams = (playbackParams != null ? playbackParams : new PlaybackParams()) - .allowDefaults(); - this.playbackParams = playbackParams; - playbackSpeed = playbackParams.getSpeed(); - maybeApplyPlaybackParams(); - } - - @Override - public float getPlaybackSpeed() { - return playbackSpeed; - } - - private void maybeApplyPlaybackParams() { - if (audioTrack != null && playbackParams != null) { - audioTrack.setPlaybackParams(playbackParams); - } + private PlaybackParametersCheckpoint(PlaybackParameters playbackParameters, long mediaTimeUs, + long positionUs) { + this.playbackParameters = playbackParameters; + this.mediaTimeUs = mediaTimeUs; + this.positionUs = positionUs; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java similarity index 94% rename from library/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java rename to library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java index e81d7e218a..b755776f1e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.audio; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C.Encoding; +import com.google.android.exoplayer2.Format; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; @@ -43,6 +44,8 @@ import java.util.Arrays; public ChannelMappingAudioProcessor() { buffer = EMPTY_BUFFER; outputBuffer = EMPTY_BUFFER; + channelCount = Format.NO_VALUE; + sampleRateHz = Format.NO_VALUE; } /** @@ -147,9 +150,13 @@ import java.util.Arrays; } @Override - public void release() { + public void reset() { flush(); buffer = EMPTY_BUFFER; + channelCount = Format.NO_VALUE; + sampleRateHz = Format.NO_VALUE; + outputChannels = null; + active = false; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java rename to library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java similarity index 98% rename from library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java rename to library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index e34068861d..48c7462b03 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -19,12 +19,12 @@ import android.annotation.TargetApi; import android.media.MediaCodec; import android.media.MediaCrypto; import android.media.MediaFormat; -import android.media.PlaybackParams; import android.media.audiofx.Virtualizer; import android.os.Handler; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; @@ -345,6 +345,16 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media return currentPositionUs; } + @Override + public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { + return audioTrack.setPlaybackParameters(playbackParameters); + } + + @Override + public PlaybackParameters getPlaybackParameters() { + return audioTrack.getPlaybackParameters(); + } + @Override protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec, ByteBuffer buffer, int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, @@ -389,9 +399,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media case C.MSG_SET_VOLUME: audioTrack.setVolume((Float) message); break; - case C.MSG_SET_PLAYBACK_PARAMS: - audioTrack.setPlaybackParams((PlaybackParams) message); - break; case C.MSG_SET_STREAM_TYPE: @C.StreamType int streamType = (Integer) message; audioTrack.setStreamType(streamType); diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java similarity index 97% rename from library/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java rename to library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java index 752f55a0ca..0dd062150d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java @@ -168,9 +168,12 @@ import java.nio.ByteOrder; } @Override - public void release() { + public void reset() { flush(); buffer = EMPTY_BUFFER; + sampleRateHz = Format.NO_VALUE; + channelCount = Format.NO_VALUE; + encoding = C.ENCODING_INVALID; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java similarity index 98% rename from library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java rename to library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index 5594d9a90e..ddb870f6ff 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.audio; -import android.media.PlaybackParams; import android.media.audiofx.Virtualizer; import android.os.Handler; import android.os.Looper; @@ -26,6 +25,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; @@ -146,7 +146,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements eventDispatcher = new EventDispatcher(eventHandler, eventListener); audioTrack = new AudioTrack(audioCapabilities, audioProcessors, new AudioTrackListener()); formatHolder = new FormatHolder(); - flagsOnlyBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); + flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance(); decoderReinitializationState = REINITIALIZATION_STATE_NONE; audioTrackNeedsConfigure = true; } @@ -434,6 +434,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements return currentPositionUs; } + @Override + public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { + return audioTrack.setPlaybackParameters(playbackParameters); + } + + @Override + public PlaybackParameters getPlaybackParameters() { + return audioTrack.getPlaybackParameters(); + } + @Override protected void onEnabled(boolean joining) throws ExoPlaybackException { decoderCounters = new DecoderCounters(); @@ -585,9 +595,6 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements case C.MSG_SET_VOLUME: audioTrack.setVolume((Float) message); break; - case C.MSG_SET_PLAYBACK_PARAMS: - audioTrack.setPlaybackParams((PlaybackParams) message); - break; case C.MSG_SET_STREAM_TYPE: @C.StreamType int streamType = (Integer) message; audioTrack.setStreamType(streamType); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java new file mode 100644 index 0000000000..5d6f01b6e0 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java @@ -0,0 +1,534 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * Copyright (C) 2010 Bill Cox, Sonic Library + * + * 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.google.android.exoplayer2.audio; + +import com.google.android.exoplayer2.util.Assertions; +import java.nio.ShortBuffer; +import java.util.Arrays; + +/** + * Sonic audio stream processor for time/pitch stretching. + *

    + * Based on https://github.com/waywardgeek/sonic. + */ +/* package */ final class Sonic { + + private static final boolean USE_CHORD_PITCH = false; + private static final int MINIMUM_PITCH = 65; + private static final int MAXIMUM_PITCH = 400; + private static final int AMDF_FREQUENCY = 4000; + + private final int sampleRate; + private final int numChannels; + private final int minPeriod; + private final int maxPeriod; + private final int maxRequired; + private final short[] downSampleBuffer; + + private int inputBufferSize; + private short[] inputBuffer; + private int outputBufferSize; + private short[] outputBuffer; + private int pitchBufferSize; + private short[] pitchBuffer; + private int oldRatePosition; + private int newRatePosition; + private float speed; + private float pitch; + private int numInputSamples; + private int numOutputSamples; + private int numPitchSamples; + private int remainingInputToCopy; + private int prevPeriod; + private int prevMinDiff; + private int minDiff; + private int maxDiff; + + /** + * Creates a new Sonic audio stream processor. + * + * @param sampleRate The sample rate of input audio. + * @param numChannels The number of channels in the input audio. + */ + public Sonic(int sampleRate, int numChannels) { + this.sampleRate = sampleRate; + this.numChannels = numChannels; + minPeriod = sampleRate / MAXIMUM_PITCH; + maxPeriod = sampleRate / MINIMUM_PITCH; + maxRequired = 2 * maxPeriod; + downSampleBuffer = new short[maxRequired]; + inputBufferSize = maxRequired; + inputBuffer = new short[maxRequired * numChannels]; + outputBufferSize = maxRequired; + outputBuffer = new short[maxRequired * numChannels]; + pitchBufferSize = maxRequired; + pitchBuffer = new short[maxRequired * numChannels]; + oldRatePosition = 0; + newRatePosition = 0; + prevPeriod = 0; + speed = 1.0f; + pitch = 1.0f; + } + + /** + * Sets the output speed. + */ + public void setSpeed(float speed) { + this.speed = speed; + } + + /** + * Gets the output speed. + */ + public float getSpeed() { + return speed; + } + + /** + * Sets the output pitch. + */ + public void setPitch(float pitch) { + this.pitch = pitch; + } + + /** + * Gets the output pitch. + */ + public float getPitch() { + return pitch; + } + + /** + * Queues remaining data from {@code buffer}, and advances its position by the number of bytes + * consumed. + * + * @param buffer A {@link ShortBuffer} containing input data between its position and limit. + */ + public void queueInput(ShortBuffer buffer) { + int samplesToWrite = buffer.remaining() / numChannels; + int bytesToWrite = samplesToWrite * numChannels * 2; + enlargeInputBufferIfNeeded(samplesToWrite); + buffer.get(inputBuffer, numInputSamples * numChannels, bytesToWrite / 2); + numInputSamples += samplesToWrite; + processStreamInput(); + } + + /** + * Gets available output, outputting to the start of {@code buffer}. The buffer's position will be + * advanced by the number of bytes written. + * + * @param buffer A {@link ShortBuffer} into which output will be written. + */ + public void getOutput(ShortBuffer buffer) { + int samplesToRead = Math.min(buffer.remaining() / numChannels, numOutputSamples); + buffer.put(outputBuffer, 0, samplesToRead * numChannels); + numOutputSamples -= samplesToRead; + System.arraycopy(outputBuffer, samplesToRead * numChannels, outputBuffer, 0, + numOutputSamples * numChannels); + } + + /** + * Forces generating output using whatever data has been queued already. No extra delay will be + * added to the output, but flushing in the middle of words could introduce distortion. + */ + public void queueEndOfStream() { + int remainingSamples = numInputSamples; + float s = speed / pitch; + int expectedOutputSamples = + numOutputSamples + (int) ((remainingSamples / s + numPitchSamples) / pitch + 0.5f); + + // Add enough silence to flush both input and pitch buffers. + enlargeInputBufferIfNeeded(remainingSamples + 2 * maxRequired); + for (int xSample = 0; xSample < 2 * maxRequired * numChannels; xSample++) { + inputBuffer[remainingSamples * numChannels + xSample] = 0; + } + numInputSamples += 2 * maxRequired; + processStreamInput(); + // Throw away any extra samples we generated due to the silence we added. + if (numOutputSamples > expectedOutputSamples) { + numOutputSamples = expectedOutputSamples; + } + // Empty input and pitch buffers. + numInputSamples = 0; + remainingInputToCopy = 0; + numPitchSamples = 0; + } + + /** + * Returns the number of output samples that can be read with {@link #getOutput(ShortBuffer)}. + */ + public int getSamplesAvailable() { + return numOutputSamples; + } + + // Internal methods. + + private void enlargeOutputBufferIfNeeded(int numSamples) { + if (numOutputSamples + numSamples > outputBufferSize) { + outputBufferSize += (outputBufferSize / 2) + numSamples; + outputBuffer = Arrays.copyOf(outputBuffer, outputBufferSize * numChannels); + } + } + + private void enlargeInputBufferIfNeeded(int numSamples) { + if (numInputSamples + numSamples > inputBufferSize) { + inputBufferSize += (inputBufferSize / 2) + numSamples; + inputBuffer = Arrays.copyOf(inputBuffer, inputBufferSize * numChannels); + } + } + + private void removeProcessedInputSamples(int position) { + int remainingSamples = numInputSamples - position; + System.arraycopy(inputBuffer, position * numChannels, inputBuffer, 0, + remainingSamples * numChannels); + numInputSamples = remainingSamples; + } + + private void copyToOutput(short[] samples, int position, int numSamples) { + enlargeOutputBufferIfNeeded(numSamples); + System.arraycopy(samples, position * numChannels, outputBuffer, numOutputSamples * numChannels, + numSamples * numChannels); + numOutputSamples += numSamples; + } + + private int copyInputToOutput(int position) { + int numSamples = Math.min(maxRequired, remainingInputToCopy); + copyToOutput(inputBuffer, position, numSamples); + remainingInputToCopy -= numSamples; + return numSamples; + } + + private void downSampleInput(short[] samples, int position, int skip) { + // If skip is greater than one, average skip samples together and write them to the down-sample + // buffer. If numChannels is greater than one, mix the channels together as we down sample. + int numSamples = maxRequired / skip; + int samplesPerValue = numChannels * skip; + position *= numChannels; + for (int i = 0; i < numSamples; i++) { + int value = 0; + for (int j = 0; j < samplesPerValue; j++) { + value += samples[position + i * samplesPerValue + j]; + } + value /= samplesPerValue; + downSampleBuffer[i] = (short) value; + } + } + + private int findPitchPeriodInRange(short[] samples, int position, int minPeriod, int maxPeriod) { + // Find the best frequency match in the range, and given a sample skip multiple. For now, just + // find the pitch of the first channel. + int bestPeriod = 0; + int worstPeriod = 255; + int minDiff = 1; + int maxDiff = 0; + position *= numChannels; + for (int period = minPeriod; period <= maxPeriod; period++) { + int diff = 0; + for (int i = 0; i < period; i++) { + short sVal = samples[position + i]; + short pVal = samples[position + period + i]; + diff += sVal >= pVal ? sVal - pVal : pVal - sVal; + } + // Note that the highest number of samples we add into diff will be less than 256, since we + // skip samples. Thus, diff is a 24 bit number, and we can safely multiply by numSamples + // without overflow. + if (diff * bestPeriod < minDiff * period) { + minDiff = diff; + bestPeriod = period; + } + if (diff * worstPeriod > maxDiff * period) { + maxDiff = diff; + worstPeriod = period; + } + } + this.minDiff = minDiff / bestPeriod; + this.maxDiff = maxDiff / worstPeriod; + return bestPeriod; + } + + /** + * Returns whether the previous pitch period estimate is a better approximation, which can occur + * at the abrupt end of voiced words. + */ + private boolean previousPeriodBetter(int minDiff, int maxDiff, boolean preferNewPeriod) { + if (minDiff == 0 || prevPeriod == 0) { + return false; + } + if (preferNewPeriod) { + if (maxDiff > minDiff * 3) { + // Got a reasonable match this period + return false; + } + if (minDiff * 2 <= prevMinDiff * 3) { + // Mismatch is not that much greater this period + return false; + } + } else { + if (minDiff <= prevMinDiff) { + return false; + } + } + return true; + } + + private int findPitchPeriod(short[] samples, int position, boolean preferNewPeriod) { + // Find the pitch period. This is a critical step, and we may have to try multiple ways to get a + // good answer. This version uses AMDF. To improve speed, we down sample by an integer factor + // get in the 11 kHz range, and then do it again with a narrower frequency range without down + // sampling. + int period; + int retPeriod; + int skip = sampleRate > AMDF_FREQUENCY ? sampleRate / AMDF_FREQUENCY : 1; + if (numChannels == 1 && skip == 1) { + period = findPitchPeriodInRange(samples, position, minPeriod, maxPeriod); + } else { + downSampleInput(samples, position, skip); + period = findPitchPeriodInRange(downSampleBuffer, 0, minPeriod / skip, maxPeriod / skip); + if (skip != 1) { + period *= skip; + int minP = period - (skip * 4); + int maxP = period + (skip * 4); + if (minP < minPeriod) { + minP = minPeriod; + } + if (maxP > maxPeriod) { + maxP = maxPeriod; + } + if (numChannels == 1) { + period = findPitchPeriodInRange(samples, position, minP, maxP); + } else { + downSampleInput(samples, position, 1); + period = findPitchPeriodInRange(downSampleBuffer, 0, minP, maxP); + } + } + } + if (previousPeriodBetter(minDiff, maxDiff, preferNewPeriod)) { + retPeriod = prevPeriod; + } else { + retPeriod = period; + } + prevMinDiff = minDiff; + prevPeriod = period; + return retPeriod; + } + + private void moveNewSamplesToPitchBuffer(int originalNumOutputSamples) { + int numSamples = numOutputSamples - originalNumOutputSamples; + if (numPitchSamples + numSamples > pitchBufferSize) { + pitchBufferSize += (pitchBufferSize / 2) + numSamples; + pitchBuffer = Arrays.copyOf(pitchBuffer, pitchBufferSize * numChannels); + } + System.arraycopy(outputBuffer, originalNumOutputSamples * numChannels, pitchBuffer, + numPitchSamples * numChannels, numSamples * numChannels); + numOutputSamples = originalNumOutputSamples; + numPitchSamples += numSamples; + } + + private void removePitchSamples(int numSamples) { + if (numSamples == 0) { + return; + } + System.arraycopy(pitchBuffer, numSamples * numChannels, pitchBuffer, 0, + (numPitchSamples - numSamples) * numChannels); + numPitchSamples -= numSamples; + } + + private void adjustPitch(int originalNumOutputSamples) { + // Latency due to pitch changes could be reduced by looking at past samples to determine pitch, + // rather than future. + if (numOutputSamples == originalNumOutputSamples) { + return; + } + moveNewSamplesToPitchBuffer(originalNumOutputSamples); + int position = 0; + while (numPitchSamples - position >= maxRequired) { + int period = findPitchPeriod(pitchBuffer, position, false); + int newPeriod = (int) (period / pitch); + enlargeOutputBufferIfNeeded(newPeriod); + if (pitch >= 1.0f) { + overlapAdd(newPeriod, numChannels, outputBuffer, numOutputSamples, pitchBuffer, position, + pitchBuffer, position + period - newPeriod); + } else { + int separation = newPeriod - period; + overlapAddWithSeparation(period, numChannels, separation, outputBuffer, numOutputSamples, + pitchBuffer, position, pitchBuffer, position); + } + numOutputSamples += newPeriod; + position += period; + } + removePitchSamples(position); + } + + private short interpolate(short[] in, int inPos, int oldSampleRate, int newSampleRate) { + short left = in[inPos * numChannels]; + short right = in[inPos * numChannels + numChannels]; + int position = newRatePosition * oldSampleRate; + int leftPosition = oldRatePosition * newSampleRate; + int rightPosition = (oldRatePosition + 1) * newSampleRate; + int ratio = rightPosition - position; + int width = rightPosition - leftPosition; + return (short) ((ratio * left + (width - ratio) * right) / width); + } + + private void adjustRate(float rate, int originalNumOutputSamples) { + if (numOutputSamples == originalNumOutputSamples) { + return; + } + int newSampleRate = (int) (sampleRate / rate); + int oldSampleRate = sampleRate; + // Set these values to help with the integer math. + while (newSampleRate > (1 << 14) || oldSampleRate > (1 << 14)) { + newSampleRate /= 2; + oldSampleRate /= 2; + } + moveNewSamplesToPitchBuffer(originalNumOutputSamples); + // Leave at least one pitch sample in the buffer. + for (int position = 0; position < numPitchSamples - 1; position++) { + while ((oldRatePosition + 1) * newSampleRate > newRatePosition * oldSampleRate) { + enlargeOutputBufferIfNeeded(1); + for (int i = 0; i < numChannels; i++) { + outputBuffer[numOutputSamples * numChannels + i] = + interpolate(pitchBuffer, position + i, oldSampleRate, newSampleRate); + } + newRatePosition++; + numOutputSamples++; + } + oldRatePosition++; + if (oldRatePosition == oldSampleRate) { + oldRatePosition = 0; + Assertions.checkState(newRatePosition == newSampleRate); + newRatePosition = 0; + } + } + removePitchSamples(numPitchSamples - 1); + } + + private int skipPitchPeriod(short[] samples, int position, float speed, int period) { + // Skip over a pitch period, and copy period/speed samples to the output. + int newSamples; + if (speed >= 2.0f) { + newSamples = (int) (period / (speed - 1.0f)); + } else { + newSamples = period; + remainingInputToCopy = (int) (period * (2.0f - speed) / (speed - 1.0f)); + } + enlargeOutputBufferIfNeeded(newSamples); + overlapAdd(newSamples, numChannels, outputBuffer, numOutputSamples, samples, position, samples, + position + period); + numOutputSamples += newSamples; + return newSamples; + } + + private int insertPitchPeriod(short[] samples, int position, float speed, int period) { + // Insert a pitch period, and determine how much input to copy directly. + int newSamples; + if (speed < 0.5f) { + newSamples = (int) (period * speed / (1.0f - speed)); + } else { + newSamples = period; + remainingInputToCopy = (int) (period * (2.0f * speed - 1.0f) / (1.0f - speed)); + } + enlargeOutputBufferIfNeeded(period + newSamples); + System.arraycopy(samples, position * numChannels, outputBuffer, numOutputSamples * numChannels, + period * numChannels); + overlapAdd(newSamples, numChannels, outputBuffer, numOutputSamples + period, samples, + position + period, samples, position); + numOutputSamples += period + newSamples; + return newSamples; + } + + private void changeSpeed(float speed) { + if (numInputSamples < maxRequired) { + return; + } + int numSamples = numInputSamples; + int position = 0; + do { + if (remainingInputToCopy > 0) { + position += copyInputToOutput(position); + } else { + int period = findPitchPeriod(inputBuffer, position, true); + if (speed > 1.0) { + position += period + skipPitchPeriod(inputBuffer, position, speed, period); + } else { + position += insertPitchPeriod(inputBuffer, position, speed, period); + } + } + } while (position + maxRequired <= numSamples); + removeProcessedInputSamples(position); + } + + private void processStreamInput() { + // Resample as many pitch periods as we have buffered on the input. + int originalNumOutputSamples = numOutputSamples; + float s = speed / pitch; + if (s > 1.00001 || s < 0.99999) { + changeSpeed(s); + } else { + copyToOutput(inputBuffer, 0, numInputSamples); + numInputSamples = 0; + } + if (USE_CHORD_PITCH) { + if (pitch != 1.0f) { + adjustPitch(originalNumOutputSamples); + } + } else if (!USE_CHORD_PITCH && pitch != 1.0f) { + adjustRate(pitch, originalNumOutputSamples); + } + } + + private static void overlapAdd(int numSamples, int numChannels, short[] out, int outPos, + short[] rampDown, int rampDownPos, short[] rampUp, int rampUpPos) { + for (int i = 0; i < numChannels; i++) { + int o = outPos * numChannels + i; + int u = rampUpPos * numChannels + i; + int d = rampDownPos * numChannels + i; + for (int t = 0; t < numSamples; t++) { + out[o] = (short) ((rampDown[d] * (numSamples - t) + rampUp[u] * t) / numSamples); + o += numChannels; + d += numChannels; + u += numChannels; + } + } + } + + private static void overlapAddWithSeparation(int numSamples, int numChannels, int separation, + short[] out, int outPos, short[] rampDown, int rampDownPos, short[] rampUp, int rampUpPos) { + for (int i = 0; i < numChannels; i++) { + int o = outPos * numChannels + i; + int u = rampUpPos * numChannels + i; + int d = rampDownPos * numChannels + i; + for (int t = 0; t < numSamples + separation; t++) { + if (t < separation) { + out[o] = (short) (rampDown[d] * (numSamples - t) / numSamples); + d += numChannels; + } else if (t < numSamples) { + out[o] = + (short) ((rampDown[d] * (numSamples - t) + rampUp[u] * (t - separation)) + / numSamples); + d += numChannels; + u += numChannels; + } else { + out[o] = (short) (rampUp[u] * (t - separation) / numSamples); + u += numChannels; + } + o += numChannels; + } + } + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java new file mode 100644 index 0000000000..df20139255 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java @@ -0,0 +1,212 @@ +/* + * 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.google.android.exoplayer2.audio; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.C.Encoding; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.util.Util; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.ShortBuffer; + +/** + * An {@link AudioProcessor} that uses the Sonic library to modify the speed/pitch of audio. + */ +public final class SonicAudioProcessor implements AudioProcessor { + + /** + * The maximum allowed playback speed in {@link #setSpeed(float)}. + */ + public static final float MAXIMUM_SPEED = 8.0f; + /** + * The minimum allowed playback speed in {@link #setSpeed(float)}. + */ + public static final float MINIMUM_SPEED = 0.1f; + /** + * The maximum allowed pitch in {@link #setPitch(float)}. + */ + public static final float MAXIMUM_PITCH = 8.0f; + /** + * The minimum allowed pitch in {@link #setPitch(float)}. + */ + public static final float MINIMUM_PITCH = 0.1f; + + /** + * The threshold below which the difference between two pitch/speed factors is negligible. + */ + private static final float CLOSE_THRESHOLD = 0.01f; + + private int channelCount; + private int sampleRateHz; + + private Sonic sonic; + private float speed; + private float pitch; + + private ByteBuffer buffer; + private ShortBuffer shortBuffer; + private ByteBuffer outputBuffer; + private long inputBytes; + private long outputBytes; + private boolean inputEnded; + + /** + * Creates a new Sonic audio processor. + */ + public SonicAudioProcessor() { + speed = 1f; + pitch = 1f; + channelCount = Format.NO_VALUE; + sampleRateHz = Format.NO_VALUE; + buffer = EMPTY_BUFFER; + shortBuffer = buffer.asShortBuffer(); + outputBuffer = EMPTY_BUFFER; + } + + /** + * Sets the playback speed. The new speed will take effect after a call to {@link #flush()}. + * + * @param speed The requested new playback speed. + * @return The actual new playback speed. + */ + public float setSpeed(float speed) { + this.speed = Util.constrainValue(speed, MINIMUM_SPEED, MAXIMUM_SPEED); + return this.speed; + } + + /** + * Sets the playback pitch. The new pitch will take effect after a call to {@link #flush()}. + * + * @param pitch The requested new pitch. + * @return The actual new pitch. + */ + public float setPitch(float pitch) { + this.pitch = Util.constrainValue(pitch, MINIMUM_PITCH, MAXIMUM_PITCH); + return pitch; + } + + /** + * Returns the number of bytes of input queued since the last call to {@link #flush()}. + */ + public long getInputByteCount() { + return inputBytes; + } + + /** + * Returns the number of bytes of output dequeued since the last call to {@link #flush()}. + */ + public long getOutputByteCount() { + return outputBytes; + } + + @Override + public boolean configure(int sampleRateHz, int channelCount, @Encoding int encoding) + throws UnhandledFormatException { + if (encoding != C.ENCODING_PCM_16BIT) { + throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + } + if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount) { + return false; + } + this.sampleRateHz = sampleRateHz; + this.channelCount = channelCount; + return true; + } + + @Override + public boolean isActive() { + return Math.abs(speed - 1f) >= CLOSE_THRESHOLD || Math.abs(pitch - 1f) >= CLOSE_THRESHOLD; + } + + @Override + public int getOutputChannelCount() { + return channelCount; + } + + @Override + public int getOutputEncoding() { + return C.ENCODING_PCM_16BIT; + } + + @Override + public void queueInput(ByteBuffer inputBuffer) { + if (inputBuffer.hasRemaining()) { + ShortBuffer shortBuffer = inputBuffer.asShortBuffer(); + int inputSize = inputBuffer.remaining(); + inputBytes += inputSize; + sonic.queueInput(shortBuffer); + inputBuffer.position(inputBuffer.position() + inputSize); + } + int outputSize = sonic.getSamplesAvailable() * channelCount * 2; + if (outputSize > 0) { + if (buffer.capacity() < outputSize) { + buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder()); + shortBuffer = buffer.asShortBuffer(); + } else { + buffer.clear(); + shortBuffer.clear(); + } + sonic.getOutput(shortBuffer); + outputBytes += outputSize; + buffer.limit(outputSize); + outputBuffer = buffer; + } + } + + @Override + public void queueEndOfStream() { + sonic.queueEndOfStream(); + inputEnded = true; + } + + @Override + public ByteBuffer getOutput() { + ByteBuffer outputBuffer = this.outputBuffer; + this.outputBuffer = EMPTY_BUFFER; + return outputBuffer; + } + + @Override + public boolean isEnded() { + return inputEnded && (sonic == null || sonic.getSamplesAvailable() == 0); + } + + @Override + public void flush() { + sonic = new Sonic(sampleRateHz, channelCount); + sonic.setSpeed(speed); + sonic.setPitch(pitch); + outputBuffer = EMPTY_BUFFER; + inputBytes = 0; + outputBytes = 0; + inputEnded = false; + } + + @Override + public void reset() { + sonic = null; + buffer = EMPTY_BUFFER; + shortBuffer = buffer.asShortBuffer(); + outputBuffer = EMPTY_BUFFER; + channelCount = Format.NO_VALUE; + sampleRateHz = Format.NO_VALUE; + inputBytes = 0; + outputBytes = 0; + inputEnded = false; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/decoder/Buffer.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/Buffer.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/decoder/Buffer.java rename to library/core/src/main/java/com/google/android/exoplayer2/decoder/Buffer.java diff --git a/library/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java similarity index 61% rename from library/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java rename to library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java index 866e421acc..0d143cdf49 100644 --- a/library/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java @@ -49,11 +49,21 @@ public final class CryptoInfo { * @see android.media.MediaCodec.CryptoInfo#numSubSamples */ public int numSubSamples; + /** + * @see android.media.MediaCodec.CryptoInfo.Pattern + */ + public int patternBlocksToEncrypt; + /** + * @see android.media.MediaCodec.CryptoInfo.Pattern + */ + public int patternBlocksToSkip; private final android.media.MediaCodec.CryptoInfo frameworkCryptoInfo; + private final PatternHolderV24 patternHolder; public CryptoInfo() { frameworkCryptoInfo = Util.SDK_INT >= 16 ? newFrameworkCryptoInfoV16() : null; + patternHolder = Util.SDK_INT >= 24 ? new PatternHolderV24(frameworkCryptoInfo) : null; } /** @@ -67,11 +77,21 @@ public final class CryptoInfo { this.key = key; this.iv = iv; this.mode = mode; + patternBlocksToEncrypt = 0; + patternBlocksToSkip = 0; if (Util.SDK_INT >= 16) { updateFrameworkCryptoInfoV16(); } } + public void setPattern(int patternBlocksToEncrypt, int patternBlocksToSkip) { + this.patternBlocksToEncrypt = patternBlocksToEncrypt; + this.patternBlocksToSkip = patternBlocksToSkip; + if (Util.SDK_INT >= 24) { + patternHolder.set(patternBlocksToEncrypt, patternBlocksToSkip); + } + } + /** * Returns an equivalent {@link android.media.MediaCodec.CryptoInfo} instance. *

    @@ -93,8 +113,35 @@ public final class CryptoInfo { @TargetApi(16) private void updateFrameworkCryptoInfoV16() { - frameworkCryptoInfo.set(numSubSamples, numBytesOfClearData, numBytesOfEncryptedData, key, iv, - mode); + // Update fields directly because the framework's CryptoInfo.set performs an unnecessary object + // allocation on Android N. + frameworkCryptoInfo.numSubSamples = numSubSamples; + frameworkCryptoInfo.numBytesOfClearData = numBytesOfClearData; + frameworkCryptoInfo.numBytesOfEncryptedData = numBytesOfEncryptedData; + frameworkCryptoInfo.key = key; + frameworkCryptoInfo.iv = iv; + frameworkCryptoInfo.mode = mode; + if (Util.SDK_INT >= 24) { + patternHolder.set(patternBlocksToEncrypt, patternBlocksToSkip); + } + } + + @TargetApi(24) + private static final class PatternHolderV24 { + + private final android.media.MediaCodec.CryptoInfo frameworkCryptoInfo; + private final android.media.MediaCodec.CryptoInfo.Pattern pattern; + + private PatternHolderV24(android.media.MediaCodec.CryptoInfo frameworkCryptoInfo) { + this.frameworkCryptoInfo = frameworkCryptoInfo; + pattern = new android.media.MediaCodec.CryptoInfo.Pattern(0, 0); + } + + private void set(int blocksToEncrypt, int blocksToSkip) { + pattern.set(blocksToEncrypt, blocksToSkip); + frameworkCryptoInfo.setPattern(pattern); + } + } } diff --git a/library/src/main/java/com/google/android/exoplayer2/decoder/Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/Decoder.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/decoder/Decoder.java rename to library/core/src/main/java/com/google/android/exoplayer2/decoder/Decoder.java diff --git a/library/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java rename to library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java diff --git a/library/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java similarity index 89% rename from library/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java rename to library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java index 84c89de427..d22a45ce88 100644 --- a/library/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java @@ -63,6 +63,15 @@ public class DecoderInputBuffer extends Buffer { @BufferReplacementMode private final int bufferReplacementMode; + /** + * Creates a new instance for which {@link #isFlagsOnly()} will return true. + * + * @return A new flags only input buffer. + */ + public static DecoderInputBuffer newFlagsOnlyInstance() { + return new DecoderInputBuffer(BUFFER_REPLACEMENT_MODE_DISABLED); + } + /** * @param bufferReplacementMode Determines the behavior of {@link #ensureSpaceForWrite(int)}. One * of {@link #BUFFER_REPLACEMENT_MODE_DISABLED}, {@link #BUFFER_REPLACEMENT_MODE_NORMAL} and @@ -109,6 +118,14 @@ public class DecoderInputBuffer extends Buffer { data = newData; } + /** + * Returns whether the buffer is only able to hold flags, meaning {@link #data} is null and + * its replacement mode is {@link #BUFFER_REPLACEMENT_MODE_DISABLED}. + */ + public final boolean isFlagsOnly() { + return data == null && bufferReplacementMode == BUFFER_REPLACEMENT_MODE_DISABLED; + } + /** * Returns whether the {@link C#BUFFER_FLAG_ENCRYPTED} flag is set. */ diff --git a/library/src/main/java/com/google/android/exoplayer2/decoder/OutputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/OutputBuffer.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/decoder/OutputBuffer.java rename to library/core/src/main/java/com/google/android/exoplayer2/decoder/OutputBuffer.java diff --git a/library/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java rename to library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java diff --git a/library/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java rename to library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/DecryptionException.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DecryptionException.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/drm/DecryptionException.java rename to library/core/src/main/java/com/google/android/exoplayer2/drm/DecryptionException.java diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java rename to library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java rename to library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java rename to library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java rename to library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/ExoMediaCrypto.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaCrypto.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/drm/ExoMediaCrypto.java rename to library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaCrypto.java diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java rename to library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java rename to library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java similarity index 97% rename from library/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java rename to library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java index 1339b90852..e6887af6da 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java @@ -23,6 +23,7 @@ import android.media.MediaDrm; import android.media.NotProvisionedException; import android.media.ResourceBusyException; import android.media.UnsupportedSchemeException; +import android.support.annotation.NonNull; import com.google.android.exoplayer2.util.Assertions; import java.util.HashMap; import java.util.Map; @@ -62,7 +63,8 @@ public final class FrameworkMediaDrm implements ExoMediaDrm listener) { mediaDrm.setOnEventListener(listener == null ? null : new MediaDrm.OnEventListener() { @Override - public void onEvent(MediaDrm md, byte[] sessionId, int event, int extra, byte[] data) { + public void onEvent(@NonNull MediaDrm md, byte[] sessionId, int event, int extra, + byte[] data) { listener.onEvent(FrameworkMediaDrm.this, sessionId, event, extra, data); } }); diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java rename to library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/KeysExpiredException.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/KeysExpiredException.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/drm/KeysExpiredException.java rename to library/core/src/main/java/com/google/android/exoplayer2/drm/KeysExpiredException.java diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/MediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/MediaDrmCallback.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/drm/MediaDrmCallback.java rename to library/core/src/main/java/com/google/android/exoplayer2/drm/MediaDrmCallback.java diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java similarity index 58% rename from library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java rename to library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index ad44574af9..040ca50c76 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -22,15 +22,9 @@ import android.os.Handler; import android.os.HandlerThread; import android.util.Pair; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.EventListener; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.Mode; import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; -import com.google.android.exoplayer2.source.dash.DashUtil; -import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; -import com.google.android.exoplayer2.source.dash.manifest.DashManifest; -import com.google.android.exoplayer2.source.dash.manifest.Period; -import com.google.android.exoplayer2.source.dash.manifest.Representation; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; import com.google.android.exoplayer2.util.Assertions; @@ -38,8 +32,7 @@ import java.io.IOException; import java.util.HashMap; /** - * Helper class to download, renew and release offline licenses. It utilizes {@link - * DefaultDrmSessionManager}. + * Helper class to download, renew and release offline licenses. */ public final class OfflineLicenseHelper { @@ -48,8 +41,8 @@ public final class OfflineLicenseHelper { private final HandlerThread handlerThread; /** - * Instantiates a new instance which uses Widevine CDM. Call {@link #releaseResources()} when - * you're done with the helper instance. + * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance + * is no longer required. * * @param licenseUrl The default license URL. * @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. @@ -64,8 +57,8 @@ public final class OfflineLicenseHelper { } /** - * Instantiates a new instance which uses Widevine CDM. Call {@link #releaseResources()} when - * you're done with the helper instance. + * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance + * is no longer required. * * @param callback Performs key and provisioning requests. * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument @@ -84,7 +77,7 @@ public final class OfflineLicenseHelper { } /** - * Constructs an instance. Call {@link #releaseResources()} when you're done with it. + * Constructs an instance. Call {@link #release()} when the instance is no longer required. * * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager. * @param callback Performs key and provisioning requests. @@ -97,7 +90,6 @@ public final class OfflineLicenseHelper { HashMap optionalKeyRequestParameters) { handlerThread = new HandlerThread("OfflineLicenseHelper"); handlerThread.start(); - conditionVariable = new ConditionVariable(); EventListener eventListener = new EventListener() { @Override @@ -124,131 +116,98 @@ public final class OfflineLicenseHelper { optionalKeyRequestParameters, new Handler(handlerThread.getLooper()), eventListener); } - /** Releases the used resources. */ - public void releaseResources() { + /** Releases the helper. Should be called when the helper is no longer required. */ + public void release() { handlerThread.quit(); } /** * Downloads an offline license. * - * @param dataSource The {@link HttpDataSource} to be used for download. - * @param manifestUriString The URI of the manifest to be read. - * @return The downloaded offline license key set id. + * @param drmInitData The {@link DrmInitData} for the content whose license is to be downloaded. + * @return The key set id for the downloaded license. * @throws IOException If an error occurs reading data from the stream. * @throws InterruptedException If the thread has been interrupted. - * @throws DrmSessionException Thrown when there is an error during DRM session. + * @throws DrmSessionException Thrown when a DRM session error occurs. */ - public byte[] download(HttpDataSource dataSource, String manifestUriString) - throws IOException, InterruptedException, DrmSessionException { - return download(dataSource, DashUtil.loadManifest(dataSource, manifestUriString)); - } - - /** - * Downloads an offline license. - * - * @param dataSource The {@link HttpDataSource} to be used for download. - * @param dashManifest The {@link DashManifest} of the DASH content. - * @return The downloaded offline license key set id. - * @throws IOException If an error occurs reading data from the stream. - * @throws InterruptedException If the thread has been interrupted. - * @throws DrmSessionException Thrown when there is an error during DRM session. - */ - public byte[] download(HttpDataSource dataSource, DashManifest dashManifest) - throws IOException, InterruptedException, DrmSessionException { - // Get DrmInitData - // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, - // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. - if (dashManifest.getPeriodCount() < 1) { - return null; - } - Period period = dashManifest.getPeriod(0); - int adaptationSetIndex = period.getAdaptationSetIndex(C.TRACK_TYPE_VIDEO); - if (adaptationSetIndex == C.INDEX_UNSET) { - adaptationSetIndex = period.getAdaptationSetIndex(C.TRACK_TYPE_AUDIO); - if (adaptationSetIndex == C.INDEX_UNSET) { - return null; - } - } - AdaptationSet adaptationSet = period.adaptationSets.get(adaptationSetIndex); - if (adaptationSet.representations.isEmpty()) { - return null; - } - Representation representation = adaptationSet.representations.get(0); - DrmInitData drmInitData = representation.format.drmInitData; - if (drmInitData == null) { - Format sampleFormat = DashUtil.loadSampleFormat(dataSource, representation); - if (sampleFormat != null) { - drmInitData = sampleFormat.drmInitData; - } - if (drmInitData == null) { - return null; - } - } - blockingKeyRequest(DefaultDrmSessionManager.MODE_DOWNLOAD, null, drmInitData); - return drmSessionManager.getOfflineLicenseKeySetId(); + public synchronized byte[] downloadLicense(DrmInitData drmInitData) throws IOException, + InterruptedException, DrmSessionException { + Assertions.checkArgument(drmInitData != null); + return blockingKeyRequest(DefaultDrmSessionManager.MODE_DOWNLOAD, null, drmInitData); } /** * Renews an offline license. * * @param offlineLicenseKeySetId The key set id of the license to be renewed. - * @return Renewed offline license key set id. - * @throws DrmSessionException Thrown when there is an error during DRM session. + * @return The renewed offline license key set id. + * @throws DrmSessionException Thrown when a DRM session error occurs. */ - public byte[] renew(byte[] offlineLicenseKeySetId) throws DrmSessionException { + public synchronized byte[] renewLicense(byte[] offlineLicenseKeySetId) + throws DrmSessionException { Assertions.checkNotNull(offlineLicenseKeySetId); - blockingKeyRequest(DefaultDrmSessionManager.MODE_DOWNLOAD, offlineLicenseKeySetId, null); - return drmSessionManager.getOfflineLicenseKeySetId(); + return blockingKeyRequest(DefaultDrmSessionManager.MODE_DOWNLOAD, offlineLicenseKeySetId, null); } /** * Releases an offline license. * * @param offlineLicenseKeySetId The key set id of the license to be released. - * @throws DrmSessionException Thrown when there is an error during DRM session. + * @throws DrmSessionException Thrown when a DRM session error occurs. */ - public void release(byte[] offlineLicenseKeySetId) throws DrmSessionException { + public synchronized void releaseLicense(byte[] offlineLicenseKeySetId) + throws DrmSessionException { Assertions.checkNotNull(offlineLicenseKeySetId); blockingKeyRequest(DefaultDrmSessionManager.MODE_RELEASE, offlineLicenseKeySetId, null); } /** - * Returns license and playback durations remaining in seconds of the given offline license. + * Returns the remaining license and playback durations in seconds, for an offline license. * * @param offlineLicenseKeySetId The key set id of the license. + * @return The remaining license and playback durations, in seconds. + * @throws DrmSessionException Thrown when a DRM session error occurs. */ - public Pair getLicenseDurationRemainingSec(byte[] offlineLicenseKeySetId) + public synchronized Pair getLicenseDurationRemainingSec(byte[] offlineLicenseKeySetId) throws DrmSessionException { Assertions.checkNotNull(offlineLicenseKeySetId); - DrmSession session = openBlockingKeyRequest(DefaultDrmSessionManager.MODE_QUERY, + DrmSession drmSession = openBlockingKeyRequest(DefaultDrmSessionManager.MODE_QUERY, offlineLicenseKeySetId, null); + DrmSessionException error = drmSession.getError(); Pair licenseDurationRemainingSec = - WidevineUtil.getLicenseDurationRemainingSec(drmSessionManager); - drmSessionManager.releaseSession(session); + WidevineUtil.getLicenseDurationRemainingSec(drmSession); + drmSessionManager.releaseSession(drmSession); + if (error != null) { + if (error.getCause() instanceof KeysExpiredException) { + return Pair.create(0L, 0L); + } + throw error; + } return licenseDurationRemainingSec; } - private void blockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId, + private byte[] blockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId, DrmInitData drmInitData) throws DrmSessionException { - DrmSession session = openBlockingKeyRequest(licenseMode, offlineLicenseKeySetId, + DrmSession drmSession = openBlockingKeyRequest(licenseMode, offlineLicenseKeySetId, drmInitData); - DrmSessionException error = session.getError(); + DrmSessionException error = drmSession.getError(); + byte[] keySetId = drmSession.getOfflineLicenseKeySetId(); + drmSessionManager.releaseSession(drmSession); if (error != null) { throw error; } - drmSessionManager.releaseSession(session); + return keySetId; } private DrmSession openBlockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId, DrmInitData drmInitData) { drmSessionManager.setMode(licenseMode, offlineLicenseKeySetId); conditionVariable.close(); - DrmSession session = drmSessionManager.acquireSession(handlerThread.getLooper(), + DrmSession drmSession = drmSessionManager.acquireSession(handlerThread.getLooper(), drmInitData); // Block current thread until key loading is finished conditionVariable.block(); - return session; + return drmSession; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/UnsupportedDrmException.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/UnsupportedDrmException.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/drm/UnsupportedDrmException.java rename to library/core/src/main/java/com/google/android/exoplayer2/drm/UnsupportedDrmException.java diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java similarity index 98% rename from library/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java rename to library/core/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java index fc80cfb6fb..e5d014f102 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java @@ -38,7 +38,7 @@ public final class WidevineUtil { * @throws IllegalStateException If called when a session isn't opened. * @param drmSession */ - public static Pair getLicenseDurationRemainingSec(DrmSession drmSession) { + public static Pair getLicenseDurationRemainingSec(DrmSession drmSession) { Map keyStatus = drmSession.queryKeyStatus(); return new Pair<>( getDurationRemainingSec(keyStatus, PROPERTY_LICENSE_DURATION_REMAINING), diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java similarity index 94% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java index 4ce430c5ff..baa5589f4b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java @@ -61,7 +61,11 @@ public final class ChunkIndex implements SeekMap { this.durationsUs = durationsUs; this.timesUs = timesUs; length = sizes.length; - durationUs = durationsUs[length - 1] + timesUs[length - 1]; + if (length > 0) { + durationUs = durationsUs[length - 1] + timesUs[length - 1]; + } else { + durationUs = 0; + } } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java new file mode 100644 index 0000000000..022ca1277d --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java @@ -0,0 +1,149 @@ +/* + * 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.google.android.exoplayer2.extractor; + +import com.google.android.exoplayer2.extractor.flv.FlvExtractor; +import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; +import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; +import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; +import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor; +import com.google.android.exoplayer2.extractor.ogg.OggExtractor; +import com.google.android.exoplayer2.extractor.ts.Ac3Extractor; +import com.google.android.exoplayer2.extractor.ts.AdtsExtractor; +import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory; +import com.google.android.exoplayer2.extractor.ts.PsExtractor; +import com.google.android.exoplayer2.extractor.ts.TsExtractor; +import com.google.android.exoplayer2.extractor.wav.WavExtractor; +import java.lang.reflect.Constructor; + +/** + * An {@link ExtractorsFactory} that provides an array of extractors for the following formats: + * + *

      + *
    • MP4, including M4A ({@link Mp4Extractor})
    • + *
    • fMP4 ({@link FragmentedMp4Extractor})
    • + *
    • Matroska and WebM ({@link MatroskaExtractor})
    • + *
    • Ogg Vorbis/FLAC ({@link OggExtractor}
    • + *
    • MP3 ({@link Mp3Extractor})
    • + *
    • AAC ({@link AdtsExtractor})
    • + *
    • MPEG TS ({@link TsExtractor})
    • + *
    • MPEG PS ({@link PsExtractor})
    • + *
    • FLV ({@link FlvExtractor})
    • + *
    • WAV ({@link WavExtractor})
    • + *
    • AC3 ({@link Ac3Extractor})
    • + *
    • FLAC (only available if the FLAC extension is built and included)
    • + *
    + */ +public final class DefaultExtractorsFactory implements ExtractorsFactory { + + private static final Constructor FLAC_EXTRACTOR_CONSTRUCTOR; + static { + Constructor flacExtractorConstructor = null; + try { + flacExtractorConstructor = + Class.forName("com.google.android.exoplayer2.ext.flac.FlacExtractor") + .asSubclass(Extractor.class).getConstructor(); + } catch (ClassNotFoundException e) { + // Extractor not found. + } catch (NoSuchMethodException e) { + // Constructor not found. + } + FLAC_EXTRACTOR_CONSTRUCTOR = flacExtractorConstructor; + } + + private @MatroskaExtractor.Flags int matroskaFlags; + private @FragmentedMp4Extractor.Flags int fragmentedMp4Flags; + private @Mp3Extractor.Flags int mp3Flags; + private @DefaultTsPayloadReaderFactory.Flags int tsFlags; + + /** + * Sets flags for {@link MatroskaExtractor} instances created by the factory. + * + * @see MatroskaExtractor#MatroskaExtractor(int) + * @param flags The flags to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setMatroskaExtractorFlags( + @MatroskaExtractor.Flags int flags) { + this.matroskaFlags = flags; + return this; + } + + /** + * Sets flags for {@link FragmentedMp4Extractor} instances created by the factory. + * + * @see FragmentedMp4Extractor#FragmentedMp4Extractor(int) + * @param flags The flags to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setFragmentedMp4ExtractorFlags( + @FragmentedMp4Extractor.Flags int flags) { + this.fragmentedMp4Flags = flags; + return this; + } + + /** + * Sets flags for {@link Mp3Extractor} instances created by the factory. + * + * @see Mp3Extractor#Mp3Extractor(int) + * @param flags The flags to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setMp3ExtractorFlags(@Mp3Extractor.Flags int flags) { + mp3Flags = flags; + return this; + } + + /** + * Sets flags for {@link DefaultTsPayloadReaderFactory}s used by {@link TsExtractor} instances + * created by the factory. + * + * @see TsExtractor#TsExtractor(int) + * @param flags The flags to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setTsExtractorFlags( + @DefaultTsPayloadReaderFactory.Flags int flags) { + tsFlags = flags; + return this; + } + + @Override + public synchronized Extractor[] createExtractors() { + Extractor[] extractors = new Extractor[FLAC_EXTRACTOR_CONSTRUCTOR == null ? 11 : 12]; + extractors[0] = new MatroskaExtractor(matroskaFlags); + extractors[1] = new FragmentedMp4Extractor(fragmentedMp4Flags); + extractors[2] = new Mp4Extractor(); + extractors[3] = new Mp3Extractor(mp3Flags); + extractors[4] = new AdtsExtractor(); + extractors[5] = new Ac3Extractor(); + extractors[6] = new TsExtractor(tsFlags); + extractors[7] = new FlvExtractor(); + extractors[8] = new OggExtractor(); + extractors[9] = new PsExtractor(); + extractors[10] = new WavExtractor(); + if (FLAC_EXTRACTOR_CONSTRUCTOR != null) { + try { + extractors[11] = FLAC_EXTRACTOR_CONSTRUCTOR.newInstance(); + } catch (Exception e) { + // Should never happen. + throw new IllegalStateException("Unexpected error creating FLAC extractor", e); + } + } + return extractors; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java similarity index 96% rename from library/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java index 8aff8858a1..1c9a148226 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java @@ -76,7 +76,6 @@ public final class DefaultTrackOutput implements TrackOutput { private long totalBytesWritten; private Allocation lastAllocation; private int lastAllocationOffset; - private boolean needKeyframe; private boolean pendingSplice; private UpstreamFormatChangedListener upstreamFormatChangeListener; @@ -92,7 +91,6 @@ public final class DefaultTrackOutput implements TrackOutput { scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE); state = new AtomicInteger(); lastAllocationOffset = allocationLength; - needKeyframe = true; } // Called by the consuming thread, but only when there is no loading thread. @@ -228,17 +226,13 @@ public final class DefaultTrackOutput implements TrackOutput { } /** - * Attempts to skip to the keyframe before or at the specified time. Succeeds only if the buffer - * contains a keyframe with a timestamp of {@code timeUs} or earlier, and if {@code timeUs} falls - * within the currently buffered media. - *

    - * This method is equivalent to {@code skipToKeyframeBefore(timeUs, false)}. - * - * @param timeUs The seek time. - * @return Whether the skip was successful. + * Skips all samples currently in the buffer. */ - public boolean skipToKeyframeBefore(long timeUs) { - return skipToKeyframeBefore(timeUs, false); + public void skipAll() { + long nextOffset = infoQueue.skipAll(); + if (nextOffset != C.POSITION_UNSET) { + dropDownstreamTo(nextOffset); + } } /** @@ -537,12 +531,6 @@ public final class DefaultTrackOutput implements TrackOutput { } pendingSplice = false; } - if (needKeyframe) { - if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0) { - return; - } - needKeyframe = false; - } timeUs += sampleOffsetUs; long absoluteOffset = totalBytesWritten - size - offset; infoQueue.commitSample(timeUs, flags, absoluteOffset, size, encryptionKey); @@ -572,7 +560,6 @@ public final class DefaultTrackOutput implements TrackOutput { totalBytesWritten = 0; lastAllocation = null; lastAllocationOffset = allocationLength; - needKeyframe = true; } /** @@ -629,6 +616,7 @@ public final class DefaultTrackOutput implements TrackOutput { private long largestDequeuedTimestampUs; private long largestQueuedTimestampUs; + private boolean upstreamKeyframeRequired; private boolean upstreamFormatRequired; private Format upstreamFormat; private int upstreamSourceId; @@ -645,6 +633,7 @@ public final class DefaultTrackOutput implements TrackOutput { largestDequeuedTimestampUs = Long.MIN_VALUE; largestQueuedTimestampUs = Long.MIN_VALUE; upstreamFormatRequired = true; + upstreamKeyframeRequired = true; } public void clearSampleData() { @@ -652,6 +641,7 @@ public final class DefaultTrackOutput implements TrackOutput { relativeReadIndex = 0; relativeWriteIndex = 0; queueSize = 0; + upstreamKeyframeRequired = true; } // Called by the consuming thread, but only when there is no loading thread. @@ -794,6 +784,10 @@ public final class DefaultTrackOutput implements TrackOutput { return C.RESULT_FORMAT_READ; } + if (buffer.isFlagsOnly()) { + return C.RESULT_NOTHING_READ; + } + buffer.timeUs = timesUs[relativeReadIndex]; buffer.setFlags(flags[relativeReadIndex]); extrasHolder.size = sizes[relativeReadIndex]; @@ -814,6 +808,24 @@ public final class DefaultTrackOutput implements TrackOutput { return C.RESULT_BUFFER_READ; } + /** + * Skips all samples in the buffer. + * + * @return The offset up to which data should be dropped, or {@link C#POSITION_UNSET} if no + * dropping of data is required. + */ + public synchronized long skipAll() { + if (queueSize == 0) { + return C.POSITION_UNSET; + } + + int lastSampleIndex = (relativeReadIndex + queueSize - 1) % capacity; + relativeReadIndex = (relativeReadIndex + queueSize) % capacity; + absoluteReadIndex += queueSize; + queueSize = 0; + return offsets[lastSampleIndex] + sizes[lastSampleIndex]; + } + /** * Attempts to locate the keyframe before or at the specified time. If * {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs} @@ -856,9 +868,9 @@ public final class DefaultTrackOutput implements TrackOutput { return C.POSITION_UNSET; } - queueSize -= sampleCountToKeyframe; relativeReadIndex = (relativeReadIndex + sampleCountToKeyframe) % capacity; absoluteReadIndex += sampleCountToKeyframe; + queueSize -= sampleCountToKeyframe; return offsets[relativeReadIndex]; } @@ -881,6 +893,12 @@ public final class DefaultTrackOutput implements TrackOutput { public synchronized void commitSample(long timeUs, @C.BufferFlags int sampleFlags, long offset, int size, byte[] encryptionKey) { + if (upstreamKeyframeRequired) { + if ((sampleFlags & C.BUFFER_FLAG_KEY_FRAME) == 0) { + return; + } + upstreamKeyframeRequired = false; + } Assertions.checkState(!upstreamFormatRequired); commitSampleTimestamp(timeUs); timesUs[relativeWriteIndex] = timeUs; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorsFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorsFactory.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorsFactory.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ExtractorsFactory.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/PositionHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/PositionHolder.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/PositionHolder.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/PositionHolder.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java similarity index 63% rename from library/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java index 3ee87b47ea..8e3bd08375 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java @@ -29,6 +29,7 @@ import java.util.Collections; */ /* package */ final class AudioTagPayloadReader extends TagPayloadReader { + private static final int AUDIO_FORMAT_MP3 = 2; private static final int AUDIO_FORMAT_ALAW = 7; private static final int AUDIO_FORMAT_ULAW = 8; private static final int AUDIO_FORMAT_AAC = 10; @@ -36,6 +37,8 @@ import java.util.Collections; private static final int AAC_PACKET_TYPE_SEQUENCE_HEADER = 0; private static final int AAC_PACKET_TYPE_AAC_RAW = 1; + private static final int[] AUDIO_SAMPLING_RATE_TABLE = new int[] {5512, 11025, 22050, 44100}; + // State variables private boolean hasParsedAudioDataHeader; private boolean hasOutputFormat; @@ -55,8 +58,14 @@ import java.util.Collections; if (!hasParsedAudioDataHeader) { int header = data.readUnsignedByte(); audioFormat = (header >> 4) & 0x0F; - // TODO: Add support for MP3. - if (audioFormat == AUDIO_FORMAT_ALAW || audioFormat == AUDIO_FORMAT_ULAW) { + if (audioFormat == AUDIO_FORMAT_MP3) { + int sampleRateIndex = (header >> 2) & 0x03; + int sampleRate = AUDIO_SAMPLING_RATE_TABLE[sampleRateIndex]; + Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_MPEG, null, + Format.NO_VALUE, Format.NO_VALUE, 1, sampleRate, null, null, 0, null); + output.format(format); + hasOutputFormat = true; + } else if (audioFormat == AUDIO_FORMAT_ALAW || audioFormat == AUDIO_FORMAT_ULAW) { String type = audioFormat == AUDIO_FORMAT_ALAW ? MimeTypes.AUDIO_ALAW : MimeTypes.AUDIO_ULAW; int pcmEncoding = (header & 0x01) == 1 ? C.ENCODING_PCM_16BIT : C.ENCODING_PCM_8BIT; @@ -77,22 +86,28 @@ import java.util.Collections; @Override protected void parsePayload(ParsableByteArray data, long timeUs) { - int packetType = data.readUnsignedByte(); - if (packetType == AAC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) { - // Parse the sequence header. - byte[] audioSpecificConfig = new byte[data.bytesLeft()]; - data.readBytes(audioSpecificConfig, 0, audioSpecificConfig.length); - Pair audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig( - audioSpecificConfig); - Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_AAC, null, - Format.NO_VALUE, Format.NO_VALUE, audioParams.second, audioParams.first, - Collections.singletonList(audioSpecificConfig), null, 0, null); - output.format(format); - hasOutputFormat = true; - } else if (audioFormat != AUDIO_FORMAT_AAC || packetType == AAC_PACKET_TYPE_AAC_RAW) { + if (audioFormat == AUDIO_FORMAT_MP3) { int sampleSize = data.bytesLeft(); output.sampleData(data, sampleSize); output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); + } else { + int packetType = data.readUnsignedByte(); + if (packetType == AAC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) { + // Parse the sequence header. + byte[] audioSpecificConfig = new byte[data.bytesLeft()]; + data.readBytes(audioSpecificConfig, 0, audioSpecificConfig.length); + Pair audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig( + audioSpecificConfig); + Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_AAC, null, + Format.NO_VALUE, Format.NO_VALUE, audioParams.second, audioParams.first, + Collections.singletonList(audioSpecificConfig), null, 0, null); + output.format(format); + hasOutputFormat = true; + } else if (audioFormat != AUDIO_FORMAT_AAC || packetType == AAC_PACKET_TYPE_AAC_RAW) { + int sampleSize = data.bytesLeft(); + output.sampleData(data, sampleSize); + output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); + } } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java similarity index 99% rename from library/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java index 7229d96300..1a4f8f3e88 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java @@ -80,8 +80,8 @@ import java.util.Map; } int type = readAmfType(data); if (type != AMF_TYPE_ECMA_ARRAY) { - // Should never happen. - throw new ParserException(); + // We're not interested in this metadata. + return; } // Set the duration to the value contained in the metadata, if present. Map metadata = readAmfEcmaArray(data); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java similarity index 98% rename from library/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java index 3ff7dd608e..8a4d314ee0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java @@ -93,7 +93,7 @@ import com.google.android.exoplayer2.video.AvcConfig; avcConfig.initializationData, Format.NO_VALUE, avcConfig.pixelWidthAspectRatio, null); output.format(format); hasOutputFormat = true; - } else if (packetType == AVC_PACKET_TYPE_AVC_NALU) { + } else if (packetType == AVC_PACKET_TYPE_AVC_NALU && hasOutputFormat) { // TODO: Deduplicate with Mp4Extractor. // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case // they're only 1 or 2 bytes long. diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReaderOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReaderOutput.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReaderOutput.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/EbmlReaderOutput.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java similarity index 87% rename from library/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 51ce819282..8f3abf4688 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.mkv; +import android.support.annotation.IntDef; import android.util.SparseArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -36,8 +37,11 @@ import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.AvcConfig; +import com.google.android.exoplayer2.video.ColorInfo; import com.google.android.exoplayer2.video.HevcConfig; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; @@ -64,6 +68,22 @@ public final class MatroskaExtractor implements Extractor { }; + /** + * Flags controlling the behavior of the extractor. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = {FLAG_DISABLE_SEEK_FOR_CUES}) + public @interface Flags {} + /** + * Flag to disable seeking for cues. + *

    + * Normally (i.e. when this flag is not set) the extractor will seek to the cues element if its + * position is specified in the seek head and if it's after the first cluster. Setting this flag + * disables seeking to the cues element. If the cues element is after the first cluster then the + * media is treated as being unseekable. + */ + public static final int FLAG_DISABLE_SEEK_FOR_CUES = 1; + private static final int UNSET_ENTRY_ID = -1; private static final int BLOCK_STATE_START = 0; @@ -99,6 +119,7 @@ public final class MatroskaExtractor implements Extractor { private static final String CODEC_ID_SUBRIP = "S_TEXT/UTF8"; private static final String CODEC_ID_VOBSUB = "S_VOBSUB"; private static final String CODEC_ID_PGS = "S_HDMV/PGS"; + private static final String CODEC_ID_DVBSUB = "S_DVBSUB"; private static final int VORBIS_MAX_INPUT_SIZE = 8192; private static final int OPUS_MAX_INPUT_SIZE = 5760; @@ -167,6 +188,23 @@ public final class MatroskaExtractor implements Extractor { private static final int ID_PROJECTION = 0x7670; private static final int ID_PROJECTION_PRIVATE = 0x7672; private static final int ID_STEREO_MODE = 0x53B8; + private static final int ID_COLOUR = 0x55B0; + private static final int ID_COLOUR_RANGE = 0x55B9; + private static final int ID_COLOUR_TRANSFER = 0x55BA; + private static final int ID_COLOUR_PRIMARIES = 0x55BB; + private static final int ID_MAX_CLL = 0x55BC; + private static final int ID_MAX_FALL = 0x55BD; + private static final int ID_MASTERING_METADATA = 0x55D0; + private static final int ID_PRIMARY_R_CHROMATICITY_X = 0x55D1; + private static final int ID_PRIMARY_R_CHROMATICITY_Y = 0x55D2; + private static final int ID_PRIMARY_G_CHROMATICITY_X = 0x55D3; + private static final int ID_PRIMARY_G_CHROMATICITY_Y = 0x55D4; + private static final int ID_PRIMARY_B_CHROMATICITY_X = 0x55D5; + private static final int ID_PRIMARY_B_CHROMATICITY_Y = 0x55D6; + private static final int ID_WHITE_POINT_CHROMATICITY_X = 0x55D7; + private static final int ID_WHITE_POINT_CHROMATICITY_Y = 0x55D8; + private static final int ID_LUMNINANCE_MAX = 0x55D9; + private static final int ID_LUMNINANCE_MIN = 0x55DA; private static final int LACING_NONE = 0; private static final int LACING_XIPH = 1; @@ -221,6 +259,7 @@ public final class MatroskaExtractor implements Extractor { private final EbmlReader reader; private final VarintReader varintReader; private final SparseArray tracks; + private final boolean seekForCuesEnabled; // Temporary arrays. private final ParsableByteArray nalStartCode; @@ -288,12 +327,17 @@ public final class MatroskaExtractor implements Extractor { private ExtractorOutput extractorOutput; public MatroskaExtractor() { - this(new DefaultEbmlReader()); + this(0); } - /* package */ MatroskaExtractor(EbmlReader reader) { + public MatroskaExtractor(@Flags int flags) { + this(new DefaultEbmlReader(), flags); + } + + /* package */ MatroskaExtractor(EbmlReader reader, @Flags int flags) { this.reader = reader; this.reader.init(new InnerEbmlReaderOutput()); + seekForCuesEnabled = (flags & FLAG_DISABLE_SEEK_FOR_CUES) == 0; varintReader = new VarintReader(); tracks = new SparseArray<>(); scratch = new ParsableByteArray(4); @@ -367,6 +411,8 @@ public final class MatroskaExtractor implements Extractor { case ID_CUE_TRACK_POSITIONS: case ID_BLOCK_GROUP: case ID_PROJECTION: + case ID_COLOUR: + case ID_MASTERING_METADATA: return EbmlReader.TYPE_MASTER; case ID_EBML_READ_VERSION: case ID_DOC_TYPE_READ_VERSION: @@ -397,6 +443,11 @@ public final class MatroskaExtractor implements Extractor { case ID_CUE_CLUSTER_POSITION: case ID_REFERENCE_BLOCK: case ID_STEREO_MODE: + case ID_COLOUR_RANGE: + case ID_COLOUR_TRANSFER: + case ID_COLOUR_PRIMARIES: + case ID_MAX_CLL: + case ID_MAX_FALL: return EbmlReader.TYPE_UNSIGNED_INT; case ID_DOC_TYPE: case ID_CODEC_ID: @@ -412,6 +463,16 @@ public final class MatroskaExtractor implements Extractor { return EbmlReader.TYPE_BINARY; case ID_DURATION: case ID_SAMPLING_FREQUENCY: + case ID_PRIMARY_R_CHROMATICITY_X: + case ID_PRIMARY_R_CHROMATICITY_Y: + case ID_PRIMARY_G_CHROMATICITY_X: + case ID_PRIMARY_G_CHROMATICITY_Y: + case ID_PRIMARY_B_CHROMATICITY_X: + case ID_PRIMARY_B_CHROMATICITY_Y: + case ID_WHITE_POINT_CHROMATICITY_X: + case ID_WHITE_POINT_CHROMATICITY_Y: + case ID_LUMNINANCE_MAX: + case ID_LUMNINANCE_MIN: return EbmlReader.TYPE_FLOAT; default: return EbmlReader.TYPE_UNKNOWN; @@ -447,7 +508,7 @@ public final class MatroskaExtractor implements Extractor { case ID_CLUSTER: if (!sentSeekMap) { // We need to build cues before parsing the cluster. - if (cuesContentPosition != C.POSITION_UNSET) { + if (seekForCuesEnabled && cuesContentPosition != C.POSITION_UNSET) { // We know where the Cues element is located. Seek to request it. seekForCues = true; } else { @@ -470,6 +531,9 @@ public final class MatroskaExtractor implements Extractor { case ID_TRACK_ENTRY: currentTrack = new Track(); break; + case ID_MASTERING_METADATA: + currentTrack.hasColorInfo = true; + break; default: break; } @@ -680,6 +744,60 @@ public final class MatroskaExtractor implements Extractor { break; } break; + case ID_COLOUR_PRIMARIES: + currentTrack.hasColorInfo = true; + switch ((int) value) { + case 1: + currentTrack.colorSpace = C.COLOR_SPACE_BT709; + break; + case 4: // BT.470M. + case 5: // BT.470BG. + case 6: // SMPTE 170M. + case 7: // SMPTE 240M. + currentTrack.colorSpace = C.COLOR_SPACE_BT601; + break; + case 9: + currentTrack.colorSpace = C.COLOR_SPACE_BT2020; + break; + default: + break; + } + break; + case ID_COLOUR_TRANSFER: + switch ((int) value) { + case 1: // BT.709. + case 6: // SMPTE 170M. + case 7: // SMPTE 240M. + currentTrack.colorTransfer = C.COLOR_TRANSFER_SDR; + break; + case 16: + currentTrack.colorTransfer = C.COLOR_TRANSFER_ST2084; + break; + case 18: + currentTrack.colorTransfer = C.COLOR_TRANSFER_HLG; + break; + default: + break; + } + break; + case ID_COLOUR_RANGE: + switch((int) value) { + case 1: // Broadcast range. + currentTrack.colorRange = C.COLOR_RANGE_LIMITED; + break; + case 2: + currentTrack.colorRange = C.COLOR_RANGE_FULL; + break; + default: + break; + } + break; + case ID_MAX_CLL: + currentTrack.maxContentLuminance = (int) value; + break; + case ID_MAX_FALL: + currentTrack.maxFrameAverageLuminance = (int) value; + break; default: break; } @@ -693,6 +811,36 @@ public final class MatroskaExtractor implements Extractor { case ID_SAMPLING_FREQUENCY: currentTrack.sampleRate = (int) value; break; + case ID_PRIMARY_R_CHROMATICITY_X: + currentTrack.primaryRChromaticityX = (float) value; + break; + case ID_PRIMARY_R_CHROMATICITY_Y: + currentTrack.primaryRChromaticityY = (float) value; + break; + case ID_PRIMARY_G_CHROMATICITY_X: + currentTrack.primaryGChromaticityX = (float) value; + break; + case ID_PRIMARY_G_CHROMATICITY_Y: + currentTrack.primaryGChromaticityY = (float) value; + break; + case ID_PRIMARY_B_CHROMATICITY_X: + currentTrack.primaryBChromaticityX = (float) value; + break; + case ID_PRIMARY_B_CHROMATICITY_Y: + currentTrack.primaryBChromaticityY = (float) value; + break; + case ID_WHITE_POINT_CHROMATICITY_X: + currentTrack.whitePointChromaticityX = (float) value; + break; + case ID_WHITE_POINT_CHROMATICITY_Y: + currentTrack.whitePointChromaticityY = (float) value; + break; + case ID_LUMNINANCE_MAX: + currentTrack.maxMasteringLuminance = (float) value; + break; + case ID_LUMNINANCE_MIN: + currentTrack.minMasteringLuminance = (float) value; + break; default: break; } @@ -1233,7 +1381,8 @@ public final class MatroskaExtractor implements Extractor { || CODEC_ID_PCM_INT_LIT.equals(codecId) || CODEC_ID_SUBRIP.equals(codecId) || CODEC_ID_VOBSUB.equals(codecId) - || CODEC_ID_PGS.equals(codecId); + || CODEC_ID_PGS.equals(codecId) + || CODEC_ID_DVBSUB.equals(codecId); } /** @@ -1303,6 +1452,16 @@ public final class MatroskaExtractor implements Extractor { private static final class Track { private static final int DISPLAY_UNIT_PIXELS = 0; + private static final int MAX_CHROMATICITY = 50000; // Defined in CTA-861.3. + /** + * Default max content light level (CLL) that should be encoded into hdrStaticInfo. + */ + private static final int DEFAULT_MAX_CLL = 1000; // nits. + + /** + * Default frame-average light level (FALL) that should be encoded into hdrStaticInfo. + */ + private static final int DEFAULT_MAX_FALL = 200; // nits. // Common elements. public String codecId; @@ -1324,6 +1483,25 @@ public final class MatroskaExtractor implements Extractor { public byte[] projectionData = null; @C.StereoMode public int stereoMode = Format.NO_VALUE; + public boolean hasColorInfo = false; + @C.ColorSpace + public int colorSpace = Format.NO_VALUE; + @C.ColorTransfer + public int colorTransfer = Format.NO_VALUE; + @C.ColorRange + public int colorRange = Format.NO_VALUE; + public int maxContentLuminance = DEFAULT_MAX_CLL; + public int maxFrameAverageLuminance = DEFAULT_MAX_FALL; + public float primaryRChromaticityX = Format.NO_VALUE; + public float primaryRChromaticityY = Format.NO_VALUE; + public float primaryGChromaticityX = Format.NO_VALUE; + public float primaryGChromaticityY = Format.NO_VALUE; + public float primaryBChromaticityX = Format.NO_VALUE; + public float primaryBChromaticityY = Format.NO_VALUE; + public float whitePointChromaticityX = Format.NO_VALUE; + public float whitePointChromaticityY = Format.NO_VALUE; + public float maxMasteringLuminance = Format.NO_VALUE; + public float minMasteringLuminance = Format.NO_VALUE; // Audio elements. Initially set to their default values. public int channelCount = 1; @@ -1461,6 +1639,12 @@ public final class MatroskaExtractor implements Extractor { case CODEC_ID_PGS: mimeType = MimeTypes.APPLICATION_PGS; break; + case CODEC_ID_DVBSUB: + mimeType = MimeTypes.APPLICATION_DVBSUBS; + // Init data: composition_page (2), ancillary_page (2) + initializationData = Collections.singletonList(new byte[] {codecPrivate[0], + codecPrivate[1], codecPrivate[2], codecPrivate[3]}); + break; default: throw new ParserException("Unrecognized codec identifier."); } @@ -1487,15 +1671,22 @@ public final class MatroskaExtractor implements Extractor { if (displayWidth != Format.NO_VALUE && displayHeight != Format.NO_VALUE) { pixelWidthHeightRatio = ((float) (height * displayWidth)) / (width * displayHeight); } + ColorInfo colorInfo = null; + if (hasColorInfo) { + byte[] hdrStaticInfo = getHdrStaticInfo(); + colorInfo = new ColorInfo(colorSpace, colorRange, colorTransfer, hdrStaticInfo); + } format = Format.createVideoSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, maxInputSize, width, height, Format.NO_VALUE, initializationData, - Format.NO_VALUE, pixelWidthHeightRatio, projectionData, stereoMode, drmInitData); + Format.NO_VALUE, pixelWidthHeightRatio, projectionData, stereoMode, colorInfo, + drmInitData); } else if (MimeTypes.APPLICATION_SUBRIP.equals(mimeType)) { type = C.TRACK_TYPE_TEXT; format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, selectionFlags, language, drmInitData); } else if (MimeTypes.APPLICATION_VOBSUB.equals(mimeType) - || MimeTypes.APPLICATION_PGS.equals(mimeType)) { + || MimeTypes.APPLICATION_PGS.equals(mimeType) + || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType)) { type = C.TRACK_TYPE_TEXT; format = Format.createImageSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, initializationData, language, drmInitData); @@ -1507,6 +1698,38 @@ public final class MatroskaExtractor implements Extractor { this.output.format(format); } + /** + * Returns the HDR Static Info as defined in CTA-861.3. + */ + private byte[] getHdrStaticInfo() { + // Are all fields present. + if (primaryRChromaticityX == Format.NO_VALUE || primaryRChromaticityY == Format.NO_VALUE + || primaryGChromaticityX == Format.NO_VALUE || primaryGChromaticityY == Format.NO_VALUE + || primaryBChromaticityX == Format.NO_VALUE || primaryBChromaticityY == Format.NO_VALUE + || whitePointChromaticityX == Format.NO_VALUE + || whitePointChromaticityY == Format.NO_VALUE || maxMasteringLuminance == Format.NO_VALUE + || minMasteringLuminance == Format.NO_VALUE) { + return null; + } + + byte[] hdrStaticInfoData = new byte[25]; + ByteBuffer hdrStaticInfo = ByteBuffer.wrap(hdrStaticInfoData); + hdrStaticInfo.put((byte) 0); // Type. + hdrStaticInfo.putShort((short) ((primaryRChromaticityX * MAX_CHROMATICITY) + 0.5f)); + hdrStaticInfo.putShort((short) ((primaryRChromaticityY * MAX_CHROMATICITY) + 0.5f)); + hdrStaticInfo.putShort((short) ((primaryGChromaticityX * MAX_CHROMATICITY) + 0.5f)); + hdrStaticInfo.putShort((short) ((primaryGChromaticityY * MAX_CHROMATICITY) + 0.5f)); + hdrStaticInfo.putShort((short) ((primaryBChromaticityX * MAX_CHROMATICITY) + 0.5f)); + hdrStaticInfo.putShort((short) ((primaryBChromaticityY * MAX_CHROMATICITY) + 0.5f)); + hdrStaticInfo.putShort((short) ((whitePointChromaticityX * MAX_CHROMATICITY) + 0.5f)); + hdrStaticInfo.putShort((short) ((whitePointChromaticityY * MAX_CHROMATICITY) + 0.5f)); + hdrStaticInfo.putShort((short) (maxMasteringLuminance + 0.5f)); + hdrStaticInfo.putShort((short) (minMasteringLuminance + 0.5f)); + hdrStaticInfo.putShort((short) maxContentLuminance); + hdrStaticInfo.putShort((short) maxFrameAverageLuminance); + return hdrStaticInfoData; + } + /** * Builds initialization data for a {@link Format} from FourCC codec private data. *

    diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/VarintReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/VarintReader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/mkv/VarintReader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/VarintReader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java similarity index 95% rename from library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 54141f2545..474ba65d86 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -611,24 +611,11 @@ import java.util.List; || childAtomType == Atom.TYPE__mp3 || childAtomType == Atom.TYPE_alac) { parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId, language, isQuickTime, drmInitData, out, i); - } else if (childAtomType == Atom.TYPE_TTML) { - out.format = Format.createTextSampleFormat(Integer.toString(trackId), - MimeTypes.APPLICATION_TTML, null, Format.NO_VALUE, 0, language, drmInitData); - } else if (childAtomType == Atom.TYPE_tx3g) { - out.format = Format.createTextSampleFormat(Integer.toString(trackId), - MimeTypes.APPLICATION_TX3G, null, Format.NO_VALUE, 0, language, drmInitData); - } else if (childAtomType == Atom.TYPE_wvtt) { - out.format = Format.createTextSampleFormat(Integer.toString(trackId), - MimeTypes.APPLICATION_MP4VTT, null, Format.NO_VALUE, 0, language, drmInitData); - } else if (childAtomType == Atom.TYPE_stpp) { - out.format = Format.createTextSampleFormat(Integer.toString(trackId), - MimeTypes.APPLICATION_TTML, null, Format.NO_VALUE, 0, language, drmInitData, - 0 /* subsample timing is absolute */); - } else if (childAtomType == Atom.TYPE_c608) { - // Defined by the QuickTime File Format specification. - out.format = Format.createTextSampleFormat(Integer.toString(trackId), - MimeTypes.APPLICATION_MP4CEA608, null, Format.NO_VALUE, 0, language, drmInitData); - out.requiredSampleTransformation = Track.TRANSFORMATION_CEA608_CDAT; + } else if (childAtomType == Atom.TYPE_TTML || childAtomType == Atom.TYPE_tx3g + || childAtomType == Atom.TYPE_wvtt || childAtomType == Atom.TYPE_stpp + || childAtomType == Atom.TYPE_c608) { + parseTextSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId, + language, drmInitData, out); } else if (childAtomType == Atom.TYPE_camm) { out.format = Format.createSampleFormat(Integer.toString(trackId), MimeTypes.APPLICATION_CAMERA_MOTION, null, Format.NO_VALUE, drmInitData); @@ -638,12 +625,49 @@ import java.util.List; return out; } + private static void parseTextSampleEntry(ParsableByteArray parent, int atomType, int position, + int atomSize, int trackId, String language, DrmInitData drmInitData, StsdData out) + throws ParserException { + parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE); + + // Default values. + List initializationData = null; + long subsampleOffsetUs = Format.OFFSET_SAMPLE_RELATIVE; + + String mimeType; + if (atomType == Atom.TYPE_TTML) { + mimeType = MimeTypes.APPLICATION_TTML; + } else if (atomType == Atom.TYPE_tx3g) { + mimeType = MimeTypes.APPLICATION_TX3G; + int sampleDescriptionLength = atomSize - Atom.HEADER_SIZE - 8; + byte[] sampleDescriptionData = new byte[sampleDescriptionLength]; + parent.readBytes(sampleDescriptionData, 0, sampleDescriptionLength); + initializationData = Collections.singletonList(sampleDescriptionData); + } else if (atomType == Atom.TYPE_wvtt) { + mimeType = MimeTypes.APPLICATION_MP4VTT; + } else if (atomType == Atom.TYPE_stpp) { + mimeType = MimeTypes.APPLICATION_TTML; + subsampleOffsetUs = 0; // Subsample timing is absolute. + } else if (atomType == Atom.TYPE_c608) { + // Defined by the QuickTime File Format specification. + mimeType = MimeTypes.APPLICATION_MP4CEA608; + out.requiredSampleTransformation = Track.TRANSFORMATION_CEA608_CDAT; + } else { + // Never happens. + throw new IllegalStateException(); + } + + out.format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null, + Format.NO_VALUE, 0, language, Format.NO_VALUE, drmInitData, subsampleOffsetUs, + initializationData); + } + private static void parseVideoSampleEntry(ParsableByteArray parent, int atomType, int position, int size, int trackId, int rotationDegrees, DrmInitData drmInitData, StsdData out, int entryIndex) throws ParserException { - parent.setPosition(position + Atom.HEADER_SIZE); + parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE); - parent.skipBytes(24); + parent.skipBytes(16); int width = parent.readUnsignedShort(); int height = parent.readUnsignedShort(); boolean pixelWidthHeightRatioFromPasp = false; @@ -738,7 +762,7 @@ import java.util.List; out.format = Format.createVideoSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, Format.NO_VALUE, width, height, Format.NO_VALUE, initializationData, - rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, drmInitData); + rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, null, drmInitData); } /** @@ -784,15 +808,14 @@ import java.util.List; private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType, int position, int size, int trackId, String language, boolean isQuickTime, DrmInitData drmInitData, StsdData out, int entryIndex) { - parent.setPosition(position + Atom.HEADER_SIZE); + parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE); int quickTimeSoundDescriptionVersion = 0; if (isQuickTime) { - parent.skipBytes(8); quickTimeSoundDescriptionVersion = parent.readUnsignedShort(); parent.skipBytes(6); } else { - parent.skipBytes(16); + parent.skipBytes(8); } int channelCount; @@ -1177,6 +1200,8 @@ import java.util.List; */ private static final class StsdData { + public static final int STSD_HEADER_SIZE = 8; + public final TrackEncryptionBox[] trackEncryptionBoxes; public Format format; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/DefaultSampleValues.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/DefaultSampleValues.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/mp4/DefaultSampleValues.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/DefaultSampleValues.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArray.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArray.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArray.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArray.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java similarity index 99% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java index e714928c20..9c9536beec 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java @@ -26,7 +26,6 @@ import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; - import java.io.IOException; /** diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java similarity index 85% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java index 248161f28f..6a1c566faf 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ts; +import android.support.annotation.IntDef; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.Ac3Util; @@ -23,12 +24,17 @@ import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Parses a continuous (E-)AC-3 byte stream and extracts individual samples. */ public final class Ac3Reader implements ElementaryStreamReader { + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATE_FINDING_SYNC, STATE_READING_HEADER, STATE_READING_SAMPLE}) + private @interface State {} private static final int STATE_FINDING_SYNC = 0; private static final int STATE_READING_HEADER = 1; private static final int STATE_READING_SAMPLE = 2; @@ -42,7 +48,7 @@ public final class Ac3Reader implements ElementaryStreamReader { private String trackFormatId; private TrackOutput output; - private int state; + @State private int state; private int bytesRead; // Used to find the header. @@ -52,7 +58,6 @@ public final class Ac3Reader implements ElementaryStreamReader { private long sampleDurationUs; private Format format; private int sampleSize; - private boolean isEac3; // Used when reading the samples. private long timeUs; @@ -125,6 +130,8 @@ public final class Ac3Reader implements ElementaryStreamReader { state = STATE_FINDING_SYNC; } break; + default: + break; } } } @@ -177,26 +184,22 @@ public final class Ac3Reader implements ElementaryStreamReader { /** * Parses the sample header. */ + @SuppressWarnings("ReferenceEquality") private void parseHeader() { - if (format == null) { - // We read ahead to distinguish between AC-3 and E-AC-3. - headerScratchBits.skipBits(40); - isEac3 = headerScratchBits.readBits(5) == 16; - headerScratchBits.setPosition(headerScratchBits.getPosition() - 45); - format = isEac3 - ? Ac3Util.parseEac3SyncframeFormat(headerScratchBits, trackFormatId, language , null) - : Ac3Util.parseAc3SyncframeFormat(headerScratchBits, trackFormatId, language, null); + headerScratchBits.setPosition(0); + Ac3Util.Ac3SyncFrameInfo frameInfo = Ac3Util.parseAc3SyncframeInfo(headerScratchBits); + if (format == null || frameInfo.channelCount != format.channelCount + || frameInfo.sampleRate != format.sampleRate + || frameInfo.mimeType != format.sampleMimeType) { + format = Format.createAudioSampleFormat(trackFormatId, frameInfo.mimeType, null, + Format.NO_VALUE, Format.NO_VALUE, frameInfo.channelCount, frameInfo.sampleRate, null, + null, 0, language); output.format(format); } - sampleSize = isEac3 ? Ac3Util.parseEAc3SyncframeSize(headerScratchBits.data) - : Ac3Util.parseAc3SyncframeSize(headerScratchBits.data); - int audioSamplesPerSyncframe = isEac3 - ? Ac3Util.parseEAc3SyncframeAudioSampleCount(headerScratchBits.data) - : Ac3Util.getAc3SyncframeAudioSampleCount(); + sampleSize = frameInfo.frameSize; // In this class a sample is an access unit (syncframe in AC-3), but the MediaFormat sample rate // specifies the number of PCM audio samples per second. - sampleDurationUs = - (int) (C.MICROS_PER_SECOND * audioSamplesPerSyncframe / format.sampleRate); + sampleDurationUs = C.MICROS_PER_SECOND * frameInfo.sampleCount / format.sampleRate; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java similarity index 93% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index e8b664d5ab..4bc7f11c1a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -39,8 +39,7 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact @IntDef(flag = true, value = {FLAG_ALLOW_NON_IDR_KEYFRAMES, FLAG_IGNORE_AAC_STREAM, FLAG_IGNORE_H264_STREAM, FLAG_DETECT_ACCESS_UNITS, FLAG_IGNORE_SPLICE_INFO_STREAM, FLAG_OVERRIDE_CAPTION_DESCRIPTORS}) - public @interface Flags { - } + public @interface Flags {} public static final int FLAG_ALLOW_NON_IDR_KEYFRAMES = 1; public static final int FLAG_IGNORE_AAC_STREAM = 1 << 1; public static final int FLAG_IGNORE_H264_STREAM = 1 << 2; @@ -54,11 +53,19 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact private final List closedCaptionFormats; public DefaultTsPayloadReaderFactory() { - this(0, Collections.emptyList()); + this(0); } /** - * @param flags A combination of {@code FLAG_*} values, which control the behavior of the created + * @param flags A combination of {@code FLAG_*} values that control the behavior of the created + * readers. + */ + public DefaultTsPayloadReaderFactory(@Flags int flags) { + this(flags, Collections.emptyList()); + } + + /** + * @param flags A combination of {@code FLAG_*} values that control the behavior of the created * readers. * @param closedCaptionFormats {@link Format}s to be exposed by payload readers for streams with * embedded closed captions when no caption service descriptors are provided. If @@ -109,6 +116,9 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact ? null : new SectionReader(new SpliceInfoSectionReader()); case TsExtractor.TS_STREAM_TYPE_ID3: return new PesReader(new Id3Reader()); + case TsExtractor.TS_STREAM_TYPE_DVBSUBS: + return new PesReader( + new DvbSubtitleReader(esInfo.dvbSubtitleInfos)); default: return null; } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java new file mode 100644 index 0000000000..e00c63a354 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java @@ -0,0 +1,121 @@ +/* + * 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.google.android.exoplayer2.extractor.ts; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.DvbSubtitleInfo; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.Collections; +import java.util.List; + +/** + * Parses DVB subtitle data and extracts individual frames. + */ +public final class DvbSubtitleReader implements ElementaryStreamReader { + + private final List subtitleInfos; + private final TrackOutput[] outputs; + + private boolean writingSample; + private int bytesToCheck; + private int sampleBytesWritten; + private long sampleTimeUs; + + /** + * @param subtitleInfos Information about the DVB subtitles associated to the stream. + */ + public DvbSubtitleReader(List subtitleInfos) { + this.subtitleInfos = subtitleInfos; + outputs = new TrackOutput[subtitleInfos.size()]; + } + + @Override + public void seek() { + writingSample = false; + } + + @Override + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + for (int i = 0; i < outputs.length; i++) { + DvbSubtitleInfo subtitleInfo = subtitleInfos.get(i); + idGenerator.generateNewId(); + TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); + output.format(Format.createImageSampleFormat(idGenerator.getFormatId(), + MimeTypes.APPLICATION_DVBSUBS, null, Format.NO_VALUE, + Collections.singletonList(subtitleInfo.initializationData), subtitleInfo.language, null)); + outputs[i] = output; + } + } + + @Override + public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + if (!dataAlignmentIndicator) { + return; + } + writingSample = true; + sampleTimeUs = pesTimeUs; + sampleBytesWritten = 0; + bytesToCheck = 2; + } + + @Override + public void packetFinished() { + if (writingSample) { + for (TrackOutput output : outputs) { + output.sampleMetadata(sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleBytesWritten, 0, null); + } + writingSample = false; + } + } + + @Override + public void consume(ParsableByteArray data) { + if (writingSample) { + if (bytesToCheck == 2 && !checkNextByte(data, 0x20)) { + // Failed to check data_identifier + return; + } + if (bytesToCheck == 1 && !checkNextByte(data, 0x00)) { + // Check and discard the subtitle_stream_id + return; + } + int dataPosition = data.getPosition(); + int bytesAvailable = data.bytesLeft(); + for (TrackOutput output : outputs) { + data.setPosition(dataPosition); + output.sampleData(data, bytesAvailable); + } + sampleBytesWritten += bytesAvailable; + } + } + + private boolean checkNextByte(ParsableByteArray data, int expectedValue) { + if (data.bytesLeft() == 0) { + return false; + } + if (data.readUnsignedByte() != expectedValue) { + writingSample = false; + } + bytesToCheck--; + return writingSample; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/NalUnitTargetBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/NalUnitTargetBuffer.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ts/NalUnitTargetBuffer.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/NalUnitTargetBuffer.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java similarity index 92% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index 65b97c8a73..df6efb722c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -27,6 +27,8 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory.Flags; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.DvbSubtitleInfo; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.util.Assertions; @@ -92,6 +94,7 @@ public final class TsExtractor implements Extractor { public static final int TS_STREAM_TYPE_H265 = 0x24; public static final int TS_STREAM_TYPE_ID3 = 0x15; public static final int TS_STREAM_TYPE_SPLICE_INFO = 0x86; + public static final int TS_STREAM_TYPE_DVBSUBS = 0x59; private static final int TS_PACKET_SIZE = 188; private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. @@ -121,7 +124,16 @@ public final class TsExtractor implements Extractor { private TsPayloadReader id3Reader; public TsExtractor() { - this(MODE_NORMAL, new TimestampAdjuster(0), new DefaultTsPayloadReaderFactory()); + this(0); + } + + /** + * @param defaultTsPayloadReaderFlags A combination of {@link DefaultTsPayloadReaderFactory} + * {@code FLAG_*} values that control the behavior of the payload readers. + */ + public TsExtractor(@Flags int defaultTsPayloadReaderFlags) { + this(MODE_NORMAL, new TimestampAdjuster(0), + new DefaultTsPayloadReaderFactory(defaultTsPayloadReaderFlags)); } /** @@ -356,6 +368,7 @@ public final class TsExtractor implements Extractor { private static final int TS_PMT_DESC_AC3 = 0x6A; private static final int TS_PMT_DESC_EAC3 = 0x7A; private static final int TS_PMT_DESC_DTS = 0x7B; + private static final int TS_PMT_DESC_DVBSUBS = 0x59; private final ParsableBitArray pmtScratch; private final int pid; @@ -406,7 +419,7 @@ public final class TsExtractor implements Extractor { if (mode == MODE_HLS && id3Reader == null) { // Setup an ID3 track regardless of whether there's a corresponding entry, in case one // appears intermittently during playback. See [Internal: b/20261500]. - EsInfo dummyEsInfo = new EsInfo(TS_STREAM_TYPE_ID3, null, new byte[0]); + EsInfo dummyEsInfo = new EsInfo(TS_STREAM_TYPE_ID3, null, null, new byte[0]); id3Reader = payloadReaderFactory.createPayloadReader(TS_STREAM_TYPE_ID3, dummyEsInfo); id3Reader.init(timestampAdjuster, output, new TrackIdGenerator(programNumber, TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE)); @@ -476,6 +489,7 @@ public final class TsExtractor implements Extractor { int descriptorsEndPosition = descriptorsStartPosition + length; int streamType = -1; String language = null; + List dvbSubtitleInfos = null; while (data.getPosition() < descriptorsEndPosition) { int descriptorTag = data.readUnsignedByte(); int descriptorLength = data.readUnsignedByte(); @@ -496,14 +510,25 @@ public final class TsExtractor implements Extractor { } else if (descriptorTag == TS_PMT_DESC_DTS) { // DTS_descriptor streamType = TS_STREAM_TYPE_DTS; } else if (descriptorTag == TS_PMT_DESC_ISO639_LANG) { - language = new String(data.data, data.getPosition(), 3).trim(); + language = data.readString(3).trim(); // Audio type is ignored. + } else if (descriptorTag == TS_PMT_DESC_DVBSUBS) { + streamType = TS_STREAM_TYPE_DVBSUBS; + dvbSubtitleInfos = new ArrayList<>(); + while (data.getPosition() < positionOfNextDescriptor) { + String dvbLanguage = data.readString(3).trim(); + int dvbSubtitlingType = data.readUnsignedByte(); + byte[] initializationData = new byte[4]; + data.readBytes(initializationData, 0, 4); + dvbSubtitleInfos.add(new DvbSubtitleInfo(dvbLanguage, dvbSubtitlingType, + initializationData)); + } } // Skip unused bytes of current descriptor. data.skipBytes(positionOfNextDescriptor - data.getPosition()); } data.setPosition(descriptorsEndPosition); - return new EsInfo(streamType, language, + return new EsInfo(streamType, language, dvbSubtitleInfos, Arrays.copyOfRange(data.data, descriptorsStartPosition, descriptorsEndPosition)); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java similarity index 84% rename from library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java index 4169e0f3a0..e7996c66c3 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java @@ -20,6 +20,8 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; +import java.util.Collections; +import java.util.List; /** * Parses TS packet payload data. @@ -60,22 +62,49 @@ public interface TsPayloadReader { public final int streamType; public final String language; + public final List dvbSubtitleInfos; public final byte[] descriptorBytes; /** * @param streamType The type of the stream as defined by the * {@link TsExtractor}{@code .TS_STREAM_TYPE_*}. * @param language The language of the stream, as defined by ISO/IEC 13818-1, section 2.6.18. + * @param dvbSubtitleInfos Information about DVB subtitles associated to the stream. * @param descriptorBytes The descriptor bytes associated to the stream. */ - public EsInfo(int streamType, String language, byte[] descriptorBytes) { + public EsInfo(int streamType, String language, List dvbSubtitleInfos, + byte[] descriptorBytes) { this.streamType = streamType; this.language = language; + this.dvbSubtitleInfos = dvbSubtitleInfos == null ? Collections.emptyList() + : Collections.unmodifiableList(dvbSubtitleInfos); this.descriptorBytes = descriptorBytes; } } + /** + * Holds information about a DVB subtitle, as defined in ETSI EN 300 468 V1.11.1 section 6.2.41. + */ + final class DvbSubtitleInfo { + + public final String language; + public final int type; + public final byte[] initializationData; + + /** + * @param language The ISO 639-2 three character language. + * @param type The subtitling type. + * @param initializationData The composition and ancillary page ids. + */ + public DvbSubtitleInfo(String language, int type, byte[] initializationData) { + this.language = language; + this.type = type; + this.initializationData = initializationData; + } + + } + /** * Generates track ids for initializing {@link TsPayloadReader}s' {@link TrackOutput}s. */ diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java rename to library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java rename to library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java diff --git a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java similarity index 96% rename from library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java rename to library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 3fbbfac652..8fb9bc9271 100644 --- a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -169,6 +169,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private final DrmSessionManager drmSessionManager; private final boolean playClearSamplesWithoutKeys; private final DecoderInputBuffer buffer; + private final DecoderInputBuffer flagsOnlyBuffer; private final FormatHolder formatHolder; private final List decodeOnlyPresentationTimestamps; private final MediaCodec.BufferInfo outputBufferInfo; @@ -227,6 +228,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { this.drmSessionManager = drmSessionManager; this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); + flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance(); formatHolder = new FormatHolder(); decodeOnlyPresentationTimestamps = new ArrayList<>(); outputBufferInfo = new MediaCodec.BufferInfo(); @@ -337,7 +339,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } String codecName = decoderInfo.name; - codecIsAdaptive = decoderInfo.adaptive; + codecIsAdaptive = decoderInfo.adaptive && !codecNeedsDisableAdaptationWorkaround(codecName); codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format); codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName); codecNeedsAdaptationWorkaround = codecNeedsAdaptationWorkaround(codecName); @@ -448,6 +450,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecReconfigurationState = RECONFIGURATION_STATE_NONE; codecReinitializationState = REINITIALIZATION_STATE_NONE; decoderCounters.decoderReleaseCount++; + buffer.data = null; try { codec.stop(); } finally { @@ -486,12 +489,12 @@ public abstract class MediaCodecRenderer extends BaseRenderer { if (format == null) { // We don't have a format yet, so try and read one. buffer.clear(); - int result = readSource(formatHolder, buffer, true); + int result = readSource(formatHolder, flagsOnlyBuffer, true); if (result == C.RESULT_FORMAT_READ) { onInputFormatChanged(formatHolder.format); } else if (result == C.RESULT_BUFFER_READ) { // End of stream read having not read a format. - Assertions.checkState(buffer.isEndOfStream()); + Assertions.checkState(flagsOnlyBuffer.isEndOfStream()); inputStreamEnded = true; processEndOfStream(); return; @@ -500,14 +503,28 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return; } } + // We have a format. maybeInitCodec(); if (codec != null) { TraceUtil.beginSection("drainAndFeed"); while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} while (feedInputBuffer()) {} TraceUtil.endSection(); - } else if (format != null) { - skipToKeyframeBefore(positionUs); + } else { + skipSource(positionUs); + // We need to read any format changes despite not having a codec so that drmSession can be + // updated, and so that we have the most recent format should the codec be initialized. We may + // also reach the end of the stream. Note that readSource will not read a sample into a + // flags-only buffer. + flagsOnlyBuffer.clear(); + int result = readSource(formatHolder, flagsOnlyBuffer, false); + if (result == C.RESULT_FORMAT_READ) { + onInputFormatChanged(formatHolder.format); + } else if (result == C.RESULT_BUFFER_READ) { + Assertions.checkState(flagsOnlyBuffer.isEndOfStream()); + inputStreamEnded = true; + processEndOfStream(); + } } decoderCounters.ensureUpdated(); } @@ -1162,7 +1179,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * * @param name The decoder name. * @param format The input format. - * @return True if the device is known to set the number of audio channels in the output format + * @return True if the decoder is known to set the number of audio channels in the output format * to 2 for the given input format, whilst only actually outputting a single channel. False * otherwise. */ @@ -1171,4 +1188,18 @@ public abstract class MediaCodecRenderer extends BaseRenderer { && "OMX.MTK.AUDIO.DECODER.MP3".equals(name); } + /** + * Returns whether the decoder is known to fail when adapting, despite advertising itself as an + * adaptive decoder. + *

    + * If true is returned then we explicitly disable adaptation for the decoder. + * + * @param name The decoder name. + * @return True if the decoder is known to fail when adapting. + */ + private static boolean codecNeedsDisableAdaptationWorkaround(String name) { + return Util.SDK_INT <= 19 && Util.MODEL.equals("ODROID-XU3") + && ("OMX.Exynos.AVC.Decoder".equals(name) || "OMX.Exynos.AVC.Decoder.secure".equals(name)); + } + } diff --git a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java rename to library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java diff --git a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java similarity index 98% rename from library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java rename to library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index a3a2543461..a09f6e26dd 100644 --- a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -228,10 +228,12 @@ public final class MediaCodecUtil { || "MP3Decoder".equals(name))) { return false; } + // Work around https://github.com/google/ExoPlayer/issues/398 if (Util.SDK_INT < 18 && "OMX.SEC.MP3.Decoder".equals(name)) { return false; } + // Work around https://github.com/google/ExoPlayer/issues/1528 if (Util.SDK_INT < 18 && "OMX.MTK.AUDIO.DECODER.AAC".equals(name) && "a70".equals(Util.DEVICE)) { @@ -268,13 +270,15 @@ public final class MediaCodecUtil { } // Work around https://github.com/google/ExoPlayer/issues/548 - // VP8 decoder on Samsung Galaxy S3/S4/S4 Mini/Tab 3 does not render video. + // VP8 decoder on Samsung Galaxy S3/S4/S4 Mini/Tab 3/Note 2 does not render video. if (Util.SDK_INT <= 19 + && "OMX.SEC.vp8.dec".equals(name) && "samsung".equals(Util.MANUFACTURER) && (Util.DEVICE.startsWith("d2") || Util.DEVICE.startsWith("serrano") - || Util.DEVICE.startsWith("jflte") || Util.DEVICE.startsWith("santos")) - && "samsung".equals(Util.MANUFACTURER) && "OMX.SEC.vp8.dec".equals(name)) { + || Util.DEVICE.startsWith("jflte") || Util.DEVICE.startsWith("santos") + || Util.DEVICE.startsWith("t0"))) { return false; } + // VP8 decoder on Samsung Galaxy S4 cannot be queried. if (Util.SDK_INT <= 19 && Util.DEVICE.startsWith("jflte") && "OMX.qcom.video.decoder.vp8".equals(name)) { diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderException.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderException.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderException.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderException.java diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java similarity index 67% rename from library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java index 414a8269d7..028a8eb893 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java @@ -58,39 +58,23 @@ public interface MetadataDecoderFactory { @Override public boolean supportsFormat(Format format) { - return getDecoderClass(format.sampleMimeType) != null; + String mimeType = format.sampleMimeType; + return MimeTypes.APPLICATION_ID3.equals(mimeType) + || MimeTypes.APPLICATION_EMSG.equals(mimeType) + || MimeTypes.APPLICATION_SCTE35.equals(mimeType); } @Override public MetadataDecoder createDecoder(Format format) { - try { - Class clazz = getDecoderClass(format.sampleMimeType); - if (clazz == null) { + switch (format.sampleMimeType) { + case MimeTypes.APPLICATION_ID3: + return new Id3Decoder(); + case MimeTypes.APPLICATION_EMSG: + return new EventMessageDecoder(); + case MimeTypes.APPLICATION_SCTE35: + return new SpliceInfoDecoder(); + default: throw new IllegalArgumentException("Attempted to create decoder for unsupported format"); - } - return clazz.asSubclass(MetadataDecoder.class).getConstructor().newInstance(); - } catch (Exception e) { - throw new IllegalStateException("Unexpected error instantiating decoder", e); - } - } - - private Class getDecoderClass(String mimeType) { - if (mimeType == null) { - return null; - } - try { - switch (mimeType) { - case MimeTypes.APPLICATION_ID3: - return Class.forName("com.google.android.exoplayer2.metadata.id3.Id3Decoder"); - case MimeTypes.APPLICATION_EMSG: - return Class.forName("com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder"); - case MimeTypes.APPLICATION_SCTE35: - return Class.forName("com.google.android.exoplayer2.metadata.scte35.SpliceInfoDecoder"); - default: - return null; - } - } catch (ClassNotFoundException e) { - return null; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataInputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataInputBuffer.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/metadata/MetadataInputBuffer.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataInputBuffer.java diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java similarity index 99% rename from library/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java index 9d6d0af60c..fbe3184c0d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.metadata.emsg; import android.os.Parcel; import android.os.Parcelable; - import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java similarity index 85% rename from library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index cbe6c65030..df3353fb18 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -64,6 +64,15 @@ public final class Id3Decoder implements MetadataDecoder { */ public static final int ID3_HEADER_LENGTH = 10; + private static final int FRAME_FLAG_V3_IS_COMPRESSED = 0x0080; + private static final int FRAME_FLAG_V3_IS_ENCRYPTED = 0x0040; + private static final int FRAME_FLAG_V3_HAS_GROUP_IDENTIFIER = 0x0020; + private static final int FRAME_FLAG_V4_IS_COMPRESSED = 0x0008; + private static final int FRAME_FLAG_V4_IS_ENCRYPTED = 0x0004; + private static final int FRAME_FLAG_V4_HAS_GROUP_IDENTIFIER = 0x0040; + private static final int FRAME_FLAG_V4_IS_UNSYNCHRONIZED = 0x0002; + private static final int FRAME_FLAG_V4_HAS_DATA_LENGTH = 0x0001; + private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0; private static final int ID3_TEXT_ENCODING_UTF_16 = 1; private static final int ID3_TEXT_ENCODING_UTF_16BE = 2; @@ -105,6 +114,7 @@ public final class Id3Decoder implements MetadataDecoder { } int startPosition = id3Data.getPosition(); + int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10; int framesSize = id3Header.framesSize; if (id3Header.isUnsynchronized) { framesSize = removeUnsynchronization(id3Data, id3Header.framesSize); @@ -112,18 +122,15 @@ public final class Id3Decoder implements MetadataDecoder { id3Data.setLimit(startPosition + framesSize); boolean unsignedIntFrameSizeHack = false; - if (id3Header.majorVersion == 4) { - if (!validateV4Frames(id3Data, false)) { - if (validateV4Frames(id3Data, true)) { - unsignedIntFrameSizeHack = true; - } else { - Log.w(TAG, "Failed to validate V4 ID3 tag"); - return null; - } + if (!validateFrames(id3Data, id3Header.majorVersion, frameHeaderSize, false)) { + if (id3Header.majorVersion == 4 && validateFrames(id3Data, 4, frameHeaderSize, true)) { + unsignedIntFrameSizeHack = true; + } else { + Log.w(TAG, "Failed to validate ID3 tag with majorVersion=" + id3Header.majorVersion); + return null; } } - int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10; while (id3Data.bytesLeft() >= frameHeaderSize) { Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data, unsignedIntFrameSizeHack, frameHeaderSize, framePredicate); @@ -190,18 +197,30 @@ public final class Id3Decoder implements MetadataDecoder { return new Id3Header(majorVersion, isUnsynchronized, framesSize); } - private static boolean validateV4Frames(ParsableByteArray id3Data, - boolean unsignedIntFrameSizeHack) { + private static boolean validateFrames(ParsableByteArray id3Data, int majorVersion, + int frameHeaderSize, boolean unsignedIntFrameSizeHack) { int startPosition = id3Data.getPosition(); try { - while (id3Data.bytesLeft() >= 10) { - int id = id3Data.readInt(); - int frameSize = id3Data.readUnsignedIntToInt(); - int flags = id3Data.readUnsignedShort(); + while (id3Data.bytesLeft() >= frameHeaderSize) { + // Read the next frame header. + int id; + long frameSize; + int flags; + if (majorVersion >= 3) { + id = id3Data.readInt(); + frameSize = id3Data.readUnsignedInt(); + flags = id3Data.readUnsignedShort(); + } else { + id = id3Data.readUnsignedInt24(); + frameSize = id3Data.readUnsignedInt24(); + flags = 0; + } + // Validate the frame header and skip to the next one. if (id == 0 && frameSize == 0 && flags == 0) { + // We've reached zero padding after the end of the final frame. return true; } else { - if (!unsignedIntFrameSizeHack) { + if (majorVersion == 4 && !unsignedIntFrameSizeHack) { // Parse the data size as a synchsafe integer, as per the spec. if ((frameSize & 0x808080L) != 0) { return false; @@ -209,11 +228,21 @@ public final class Id3Decoder implements MetadataDecoder { frameSize = (frameSize & 0xFF) | (((frameSize >> 8) & 0xFF) << 7) | (((frameSize >> 16) & 0xFF) << 14) | (((frameSize >> 24) & 0xFF) << 21); } + boolean hasGroupIdentifier = false; + boolean hasDataLength = false; + if (majorVersion == 4) { + hasGroupIdentifier = (flags & FRAME_FLAG_V4_HAS_GROUP_IDENTIFIER) != 0; + hasDataLength = (flags & FRAME_FLAG_V4_HAS_DATA_LENGTH) != 0; + } else if (majorVersion == 3) { + hasGroupIdentifier = (flags & FRAME_FLAG_V3_HAS_GROUP_IDENTIFIER) != 0; + // A V3 frame has data length if and only if it's compressed. + hasDataLength = (flags & FRAME_FLAG_V3_IS_COMPRESSED) != 0; + } int minimumFrameSize = 0; - if ((flags & 0x0040) != 0 /* hasGroupIdentifier */) { + if (hasGroupIdentifier) { minimumFrameSize++; } - if ((flags & 0x0001) != 0 /* hasDataLength */) { + if (hasDataLength) { minimumFrameSize += 4; } if (frameSize < minimumFrameSize) { @@ -222,7 +251,7 @@ public final class Id3Decoder implements MetadataDecoder { if (id3Data.bytesLeft() < frameSize) { return false; } - id3Data.skipBytes(frameSize); // flags + id3Data.skipBytes((int) frameSize); // flags } } return true; @@ -280,16 +309,17 @@ public final class Id3Decoder implements MetadataDecoder { boolean hasDataLength = false; boolean hasGroupIdentifier = false; if (majorVersion == 3) { - isCompressed = (flags & 0x0080) != 0; - isEncrypted = (flags & 0x0040) != 0; - hasGroupIdentifier = (flags & 0x0020) != 0; + isCompressed = (flags & FRAME_FLAG_V3_IS_COMPRESSED) != 0; + isEncrypted = (flags & FRAME_FLAG_V3_IS_ENCRYPTED) != 0; + hasGroupIdentifier = (flags & FRAME_FLAG_V3_HAS_GROUP_IDENTIFIER) != 0; + // A V3 frame has data length if and only if it's compressed. hasDataLength = isCompressed; } else if (majorVersion == 4) { - hasGroupIdentifier = (flags & 0x0040) != 0; - isCompressed = (flags & 0x0008) != 0; - isEncrypted = (flags & 0x0004) != 0; - isUnsynchronized = (flags & 0x0002) != 0; - hasDataLength = (flags & 0x0001) != 0; + hasGroupIdentifier = (flags & FRAME_FLAG_V4_HAS_GROUP_IDENTIFIER) != 0; + isCompressed = (flags & FRAME_FLAG_V4_IS_COMPRESSED) != 0; + isEncrypted = (flags & FRAME_FLAG_V4_IS_ENCRYPTED) != 0; + isUnsynchronized = (flags & FRAME_FLAG_V4_IS_UNSYNCHRONIZED) != 0; + hasDataLength = (flags & FRAME_FLAG_V4_HAS_DATA_LENGTH) != 0; } if (isCompressed || isEncrypted) { @@ -316,17 +346,13 @@ public final class Id3Decoder implements MetadataDecoder { && (majorVersion == 2 || frameId3 == 'X')) { frame = decodeTxxxFrame(id3Data, frameSize); } else if (frameId0 == 'T') { - String id = majorVersion == 2 - ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2) - : String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3); + String id = getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3); frame = decodeTextInformationFrame(id3Data, frameSize, id); } else if (frameId0 == 'W' && frameId1 == 'X' && frameId2 == 'X' && (majorVersion == 2 || frameId3 == 'X')) { frame = decodeWxxxFrame(id3Data, frameSize); } else if (frameId0 == 'W') { - String id = majorVersion == 2 - ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2) - : String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3); + String id = getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3); frame = decodeUrlLinkFrame(id3Data, frameSize, id); } else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') { frame = decodePrivFrame(id3Data, frameSize); @@ -346,11 +372,14 @@ public final class Id3Decoder implements MetadataDecoder { frame = decodeChapterTOCFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack, frameHeaderSize, framePredicate); } else { - String id = majorVersion == 2 - ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2) - : String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3); + String id = getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3); frame = decodeBinaryFrame(id3Data, frameSize, id); } + if (frame == null) { + Log.w(TAG, "Failed to decode frame: id=" + + getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3) + ", frameSize=" + + frameSize); + } return frame; } catch (UnsupportedEncodingException e) { Log.w(TAG, "Unsupported character encoding"); @@ -362,6 +391,11 @@ public final class Id3Decoder implements MetadataDecoder { private static TextInformationFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { + if (frameSize < 1) { + // Frame is malformed. + return null; + } + int encoding = id3Data.readUnsignedByte(); String charset = getCharsetName(encoding); @@ -385,9 +419,9 @@ public final class Id3Decoder implements MetadataDecoder { private static TextInformationFrame decodeTextInformationFrame(ParsableByteArray id3Data, int frameSize, String id) throws UnsupportedEncodingException { - if (frameSize <= 1) { - // Frame is empty or contains only the text encoding byte. - return new TextInformationFrame(id, null, ""); + if (frameSize < 1) { + // Frame is malformed. + return null; } int encoding = id3Data.readUnsignedByte(); @@ -404,6 +438,11 @@ public final class Id3Decoder implements MetadataDecoder { private static UrlLinkFrame decodeWxxxFrame(ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { + if (frameSize < 1) { + // Frame is malformed. + return null; + } + int encoding = id3Data.readUnsignedByte(); String charset = getCharsetName(encoding); @@ -427,11 +466,6 @@ public final class Id3Decoder implements MetadataDecoder { private static UrlLinkFrame decodeUrlLinkFrame(ParsableByteArray id3Data, int frameSize, String id) throws UnsupportedEncodingException { - if (frameSize == 0) { - // Frame is empty. - return new UrlLinkFrame(id, null, ""); - } - byte[] data = new byte[frameSize]; id3Data.readBytes(data, 0, frameSize); @@ -443,19 +477,19 @@ public final class Id3Decoder implements MetadataDecoder { private static PrivFrame decodePrivFrame(ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { - if (frameSize == 0) { - // Frame is empty. - return new PrivFrame("", new byte[0]); - } - byte[] data = new byte[frameSize]; id3Data.readBytes(data, 0, frameSize); int ownerEndIndex = indexOfZeroByte(data, 0); String owner = new String(data, 0, ownerEndIndex, "ISO-8859-1"); + byte[] privateData; int privateDataStartIndex = ownerEndIndex + 1; - byte[] privateData = Arrays.copyOfRange(data, privateDataStartIndex, data.length); + if (privateDataStartIndex < data.length) { + privateData = Arrays.copyOfRange(data, privateDataStartIndex, data.length); + } else { + privateData = new byte[0]; + } return new PrivFrame(owner, privateData); } @@ -526,6 +560,11 @@ public final class Id3Decoder implements MetadataDecoder { private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { + if (frameSize < 4) { + // Frame is malformed. + return null; + } + int encoding = id3Data.readUnsignedByte(); String charset = getCharsetName(encoding); @@ -671,6 +710,12 @@ public final class Id3Decoder implements MetadataDecoder { } } + private static String getFrameId(int majorVersion, int frameId0, int frameId1, int frameId2, + int frameId3) { + return majorVersion == 2 ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2) + : String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3); + } + private static int indexOfEos(byte[] data, int fromIndex, int encoding) { int terminationPos = indexOfZeroByte(data, fromIndex); diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceNullCommand.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceNullCommand.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceNullCommand.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceNullCommand.java diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceScheduleCommand.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceScheduleCommand.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceScheduleCommand.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceScheduleCommand.java diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java rename to library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/BehindLiveWindowException.java b/library/core/src/main/java/com/google/android/exoplayer2/source/BehindLiveWindowException.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/BehindLiveWindowException.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/BehindLiveWindowException.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java similarity index 98% rename from library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index 102a689742..e14930c7b8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -262,8 +262,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb } @Override - public void skipToKeyframeBefore(long timeUs) { - stream.skipToKeyframeBefore(startUs + timeUs); + public void skipData(long positionUs) { + stream.skipData(startUs + positionUs); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java similarity index 96% rename from library/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index 68552c99ed..9fc499f251 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.HashMap; @@ -152,12 +153,14 @@ public final class ConcatenatingMediaSource implements MediaSource { public ConcatenatedTimeline(Timeline[] timelines) { int[] sourcePeriodOffsets = new int[timelines.length]; int[] sourceWindowOffsets = new int[timelines.length]; - int periodCount = 0; + long periodCount = 0; int windowCount = 0; for (int i = 0; i < timelines.length; i++) { Timeline timeline = timelines[i]; periodCount += timeline.getPeriodCount(); - sourcePeriodOffsets[i] = periodCount; + Assertions.checkState(periodCount <= Integer.MAX_VALUE, + "ConcatenatingMediaSource children contain too many periods"); + sourcePeriodOffsets[i] = (int) periodCount; windowCount += timeline.getWindowCount(); sourceWindowOffsets[i] = windowCount; } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java similarity index 96% rename from library/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java index eb94351f61..7aab22d8a0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java @@ -43,7 +43,7 @@ public final class EmptySampleStream implements SampleStream { } @Override - public void skipToKeyframeBefore(long timeUs) { + public void skipData(long positionUs) { // Do nothing. } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java similarity index 98% rename from library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index d843943710..f247d4dd37 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -301,7 +301,7 @@ import java.io.IOException; boolean seekInsideBuffer = !isPendingReset(); for (int i = 0; seekInsideBuffer && i < trackCount; i++) { if (trackEnabledStates[i]) { - seekInsideBuffer = sampleQueues.valueAt(i).skipToKeyframeBefore(positionUs); + seekInsideBuffer = sampleQueues.valueAt(i).skipToKeyframeBefore(positionUs, false); } } // If we failed to seek within the sample queues, we need to restart. @@ -340,6 +340,15 @@ import java.io.IOException; loadingFinished, lastSeekPositionUs); } + /* package */ void skipData(int track, long positionUs) { + DefaultTrackOutput sampleQueue = sampleQueues.valueAt(track); + if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { + sampleQueue.skipAll(); + } else { + sampleQueue.skipToKeyframeBefore(positionUs, true); + } + } + // Loader.Callback implementation. @Override @@ -565,8 +574,8 @@ import java.io.IOException; } @Override - public void skipToKeyframeBefore(long timeUs) { - sampleQueues.valueAt(track).skipToKeyframeBefore(timeUs); + public void skipData(long positionUs) { + ExtractorMediaPeriod.this.skipData(track, positionUs); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java similarity index 88% rename from library/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index d893d60262..8b14c78234 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -29,6 +29,13 @@ import java.io.IOException; */ public final class LoopingMediaSource implements MediaSource { + /** + * The maximum number of periods that can be exposed by the source. The value of this constant is + * large enough to cause indefinite looping in practice (the total duration of the looping source + * will be approximately five years if the duration of each period is one second). + */ + public static final int MAX_EXPOSED_PERIODS = 157680000; + private static final String TAG = "LoopingMediaSource"; private final MediaSource childSource; @@ -50,8 +57,8 @@ public final class LoopingMediaSource implements MediaSource { * * @param childSource The {@link MediaSource} to loop. * @param loopCount The desired number of loops. Must be strictly positive. The actual number of - * loops will be capped at the maximum value that can achieved without causing the number of - * periods exposed by the source to exceed {@link Integer#MAX_VALUE}. + * loops will be capped at the maximum that can achieved without causing the number of + * periods exposed by the source to exceed {@link #MAX_EXPOSED_PERIODS}. */ public LoopingMediaSource(MediaSource childSource, int loopCount) { Assertions.checkArgument(loopCount > 0); @@ -101,8 +108,9 @@ public final class LoopingMediaSource implements MediaSource { this.childTimeline = childTimeline; childPeriodCount = childTimeline.getPeriodCount(); childWindowCount = childTimeline.getWindowCount(); - // This is the maximum number of loops that can be performed without overflow. - int maxLoopCount = Integer.MAX_VALUE / childPeriodCount; + // This is the maximum number of loops that can be performed without exceeding + // MAX_EXPOSED_PERIODS periods. + int maxLoopCount = MAX_EXPOSED_PERIODS / childPeriodCount; if (loopCount > maxLoopCount) { if (loopCount != Integer.MAX_VALUE) { Log.w(TAG, "Capped loops to avoid overflow: " + loopCount + " -> " + maxLoopCount); @@ -158,6 +166,7 @@ public final class LoopingMediaSource implements MediaSource { int periodIndexOffset = loopCount * childPeriodCount; return childTimeline.getIndexOfPeriod(loopCountAndChildUid.second) + periodIndexOffset; } + } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/MediaSource.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/SampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleStream.java similarity index 93% rename from library/src/main/java/com/google/android/exoplayer2/source/SampleStream.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/SampleStream.java index e3039878f8..dc58c29c22 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/SampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleStream.java @@ -66,10 +66,11 @@ public interface SampleStream { int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired); /** - * Attempts to skip to the keyframe before the specified time. + * Attempts to skip to the keyframe before the specified position, or to the end of the stream if + * {@code positionUs} is beyond it. * - * @param timeUs The specified time. + * @param positionUs The specified time. */ - void skipToKeyframeBefore(long timeUs); + void skipData(long positionUs); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java similarity index 99% rename from library/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java index ae367ef14c..447839392e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java @@ -99,7 +99,7 @@ public final class SinglePeriodTimeline extends Timeline { public Period getPeriod(int periodIndex, Period period, boolean setIds) { Assertions.checkIndex(periodIndex, 0, 1); Object id = setIds ? ID : null; - return period.set(id, id, 0, periodDurationUs, -windowPositionInPeriodUs); + return period.set(id, id, 0, periodDurationUs, -windowPositionInPeriodUs, false); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java similarity index 98% rename from library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index 5b717e51da..8e38588e89 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -235,8 +235,10 @@ import java.util.Arrays; } @Override - public void skipToKeyframeBefore(long timeUs) { - // Do nothing. + public void skipData(long positionUs) { + if (positionUs > 0) { + streamState = STREAM_STATE_END_OF_STREAM; + } } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/UnrecognizedInputFormatException.java b/library/core/src/main/java/com/google/android/exoplayer2/source/UnrecognizedInputFormatException.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/UnrecognizedInputFormatException.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/UnrecognizedInputFormatException.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java similarity index 97% rename from library/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java index 7a5aeabeb6..62c07ee248 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.source.chunk; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.extractor.DefaultTrackOutput; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkHolder.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkHolder.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkHolder.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java similarity index 96% rename from library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index 93d86a8de1..c43f3d577a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -193,7 +193,7 @@ public class ChunkSampleStream implements SampleStream, S // TODO: For this to work correctly, the embedded streams must not discard anything from their // sample queues beyond the current read position of the primary stream. for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { - embeddedSampleQueue.skipToKeyframeBefore(positionUs); + embeddedSampleQueue.skipToKeyframeBefore(positionUs, true); } } else { // We failed, and need to restart. @@ -251,8 +251,12 @@ public class ChunkSampleStream implements SampleStream, S } @Override - public void skipToKeyframeBefore(long timeUs) { - primarySampleQueue.skipToKeyframeBefore(timeUs); + public void skipData(long positionUs) { + if (loadingFinished && positionUs > primarySampleQueue.getLargestQueuedTimestampUs()) { + primarySampleQueue.skipAll(); + } else { + primarySampleQueue.skipToKeyframeBefore(positionUs, true); + } } // Loader.Callback implementation. @@ -448,8 +452,12 @@ public class ChunkSampleStream implements SampleStream, S } @Override - public void skipToKeyframeBefore(long timeUs) { - sampleQueue.skipToKeyframeBefore(timeUs); + public void skipData(long positionUs) { + if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { + sampleQueue.skipAll(); + } else { + sampleQueue.skipToKeyframeBefore(positionUs, true); + } } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkedTrackBlacklistUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkedTrackBlacklistUtil.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkedTrackBlacklistUtil.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkedTrackBlacklistUtil.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/DataChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/DataChunk.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/chunk/DataChunk.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/chunk/DataChunk.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java b/library/core/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/Cue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java similarity index 91% rename from library/src/main/java/com/google/android/exoplayer2/text/Cue.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java index 176b8ea815..5ae1f35b7e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/Cue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java @@ -167,6 +167,13 @@ public class Cue { */ public final float size; + /** + * The bitmap height as a fraction of the of the viewport size, or {@link #DIMEN_UNSET} if the + * bitmap should be displayed at its natural height given the bitmap dimensions and the specified + * {@link #size}. + */ + public final float bitmapHeight; + /** * Specifies whether or not the {@link #windowColor} property is set. */ @@ -189,12 +196,15 @@ public class Cue { * fraction of the viewport height. * @param verticalPositionAnchor The vertical anchor. One of {@link #ANCHOR_TYPE_START}, * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. - * @param width The width of the cue, expressed as a fraction of the viewport width. + * @param width The width of the cue as a fraction of the viewport width. + * @param height The height of the cue as a fraction of the viewport height, or + * {@link #DIMEN_UNSET} if the bitmap should be displayed at its natural height for the + * specified {@code width}. */ public Cue(Bitmap bitmap, float horizontalPosition, @AnchorType int horizontalPositionAnchor, - float verticalPosition, @AnchorType int verticalPositionAnchor, float width) { + float verticalPosition, @AnchorType int verticalPositionAnchor, float width, float height) { this(null, null, bitmap, verticalPosition, LINE_TYPE_FRACTION, verticalPositionAnchor, - horizontalPosition, horizontalPositionAnchor, width, false, Color.BLACK); + horizontalPosition, horizontalPositionAnchor, width, height, false, Color.BLACK); } /** @@ -243,12 +253,13 @@ public class Cue { @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size, boolean windowColorSet, int windowColor) { this(text, textAlignment, null, line, lineType, lineAnchor, position, positionAnchor, size, - windowColorSet, windowColor); + DIMEN_UNSET, windowColorSet, windowColor); } private Cue(CharSequence text, Alignment textAlignment, Bitmap bitmap, float line, @LineType int lineType, @AnchorType int lineAnchor, float position, - @AnchorType int positionAnchor, float size, boolean windowColorSet, int windowColor) { + @AnchorType int positionAnchor, float size, float bitmapHeight, boolean windowColorSet, + int windowColor) { this.text = text; this.textAlignment = textAlignment; this.bitmap = bitmap; @@ -258,6 +269,7 @@ public class Cue { this.position = position; this.positionAnchor = positionAnchor; this.size = size; + this.bitmapHeight = bitmapHeight; this.windowColorSet = windowColorSet; this.windowColor = windowColor; } diff --git a/library/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java similarity index 93% rename from library/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java index dd25ef8345..6955f775dd 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java @@ -67,7 +67,7 @@ public abstract class SimpleSubtitleDecoder extends SubtitleOutputBuffer outputBuffer, boolean reset) { try { ByteBuffer inputData = inputBuffer.data; - Subtitle subtitle = decode(inputData.array(), inputData.limit()); + Subtitle subtitle = decode(inputData.array(), inputData.limit(), reset); outputBuffer.setContent(inputBuffer.timeUs, subtitle, inputBuffer.subsampleOffsetUs); // Clear BUFFER_FLAG_DECODE_ONLY (see [Internal: b/27893809]). outputBuffer.clearFlag(C.BUFFER_FLAG_DECODE_ONLY); @@ -82,9 +82,11 @@ public abstract class SimpleSubtitleDecoder extends * * @param data An array holding the data to be decoded, starting at position 0. * @param size The size of the data to be decoded. + * @param reset Whether the decoder must be reset before decoding. * @return The decoded {@link Subtitle}. * @throws SubtitleDecoderException If a decoding error occurs. */ - protected abstract Subtitle decode(byte[] data, int size) throws SubtitleDecoderException; + protected abstract Subtitle decode(byte[] data, int size, boolean reset) + throws SubtitleDecoderException; } diff --git a/library/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleOutputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleOutputBuffer.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleOutputBuffer.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleOutputBuffer.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/Subtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/Subtitle.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/text/Subtitle.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/Subtitle.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoder.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoder.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoder.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderException.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderException.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderException.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderException.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java similarity index 54% rename from library/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java index 077fc8848b..795189e1a6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.text; import com.google.android.exoplayer2.Format; 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.subrip.SubripDecoder; import com.google.android.exoplayer2.text.ttml.TtmlDecoder; import com.google.android.exoplayer2.text.tx3g.Tx3gDecoder; @@ -60,63 +61,47 @@ public interface SubtitleDecoderFactory { *

  • TX3G ({@link Tx3gDecoder})
  • *
  • Cea608 ({@link Cea608Decoder})
  • *
  • Cea708 ({@link Cea708Decoder})
  • + *
  • DVB ({@link DvbDecoder})
  • *
*/ SubtitleDecoderFactory DEFAULT = new SubtitleDecoderFactory() { @Override public boolean supportsFormat(Format format) { - return getDecoderClass(format.sampleMimeType) != null; + String mimeType = format.sampleMimeType; + return MimeTypes.TEXT_VTT.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); } @Override public SubtitleDecoder createDecoder(Format format) { - try { - Class clazz = getDecoderClass(format.sampleMimeType); - if (clazz == null) { + switch (format.sampleMimeType) { + case MimeTypes.TEXT_VTT: + return new WebvttDecoder(); + 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); + case MimeTypes.APPLICATION_DVBSUBS: + return new DvbDecoder(format.initializationData); + default: throw new IllegalArgumentException("Attempted to create decoder for unsupported format"); - } - if (format.sampleMimeType.equals(MimeTypes.APPLICATION_CEA608) - || format.sampleMimeType.equals(MimeTypes.APPLICATION_MP4CEA608)) { - return clazz.asSubclass(SubtitleDecoder.class).getConstructor(String.class, Integer.TYPE) - .newInstance(format.sampleMimeType, format.accessibilityChannel); - } else if (format.sampleMimeType.equals(MimeTypes.APPLICATION_CEA708)) { - return clazz.asSubclass(SubtitleDecoder.class).getConstructor(Integer.TYPE) - .newInstance(format.accessibilityChannel); - } else { - return clazz.asSubclass(SubtitleDecoder.class).getConstructor().newInstance(); - } - } catch (Exception e) { - throw new IllegalStateException("Unexpected error instantiating decoder", e); - } - } - - private Class getDecoderClass(String mimeType) { - if (mimeType == null) { - return null; - } - try { - switch (mimeType) { - case MimeTypes.TEXT_VTT: - return Class.forName("com.google.android.exoplayer2.text.webvtt.WebvttDecoder"); - case MimeTypes.APPLICATION_TTML: - return Class.forName("com.google.android.exoplayer2.text.ttml.TtmlDecoder"); - case MimeTypes.APPLICATION_MP4VTT: - return Class.forName("com.google.android.exoplayer2.text.webvtt.Mp4WebvttDecoder"); - case MimeTypes.APPLICATION_SUBRIP: - return Class.forName("com.google.android.exoplayer2.text.subrip.SubripDecoder"); - case MimeTypes.APPLICATION_TX3G: - return Class.forName("com.google.android.exoplayer2.text.tx3g.Tx3gDecoder"); - case MimeTypes.APPLICATION_CEA608: - case MimeTypes.APPLICATION_MP4CEA608: - return Class.forName("com.google.android.exoplayer2.text.cea.Cea608Decoder"); - case MimeTypes.APPLICATION_CEA708: - return Class.forName("com.google.android.exoplayer2.text.cea.Cea708Decoder"); - default: - return null; - } - } catch (ClassNotFoundException e) { - return null; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/text/SubtitleInputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleInputBuffer.java similarity index 93% rename from library/src/main/java/com/google/android/exoplayer2/text/SubtitleInputBuffer.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleInputBuffer.java index 8bb9e790eb..28e67e8623 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/SubtitleInputBuffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleInputBuffer.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text; +import android.support.annotation.NonNull; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; @@ -35,7 +36,7 @@ public final class SubtitleInputBuffer extends DecoderInputBuffer } @Override - public int compareTo(SubtitleInputBuffer other) { + public int compareTo(@NonNull SubtitleInputBuffer other) { long delta = timeUs - other.timeUs; if (delta == 0) { return 0; diff --git a/library/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java similarity index 77% rename from library/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java index a7e05a010a..2f07fe5294 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java @@ -19,6 +19,7 @@ import android.os.Handler; import android.os.Handler.Callback; import android.os.Looper; import android.os.Message; +import android.support.annotation.IntDef; import com.google.android.exoplayer2.BaseRenderer; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -26,6 +27,8 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Collections; import java.util.List; @@ -52,6 +55,27 @@ public final class TextRenderer extends BaseRenderer implements Callback { } + @Retention(RetentionPolicy.SOURCE) + @IntDef({REPLACEMENT_STATE_NONE, REPLACEMENT_STATE_SIGNAL_END_OF_STREAM, + REPLACEMENT_STATE_WAIT_END_OF_STREAM}) + private @interface ReplacementState {} + /** + * The decoder does not need to be replaced. + */ + private static final int REPLACEMENT_STATE_NONE = 0; + /** + * The decoder needs to be replaced, but we haven't yet signaled an end of stream to the existing + * decoder. We need to do so in order to ensure that it outputs any remaining buffers before we + * release it. + */ + private static final int REPLACEMENT_STATE_SIGNAL_END_OF_STREAM = 1; + /** + * The decoder needs to be replaced, and we've signaled an end of stream to the existing decoder. + * We're waiting for the decoder to output an end of stream signal to indicate that it has output + * any remaining buffers before we release it. + */ + private static final int REPLACEMENT_STATE_WAIT_END_OF_STREAM = 2; + private static final int MSG_UPDATE_OUTPUT = 0; private final Handler outputHandler; @@ -61,6 +85,8 @@ public final class TextRenderer extends BaseRenderer implements Callback { private boolean inputStreamEnded; private boolean outputStreamEnded; + @ReplacementState private int decoderReplacementState; + private Format streamFormat; private SubtitleDecoder decoder; private SubtitleInputBuffer nextInputBuffer; private SubtitleOutputBuffer subtitle; @@ -105,20 +131,25 @@ public final class TextRenderer extends BaseRenderer implements Callback { @Override protected void onStreamChanged(Format[] formats) throws ExoPlaybackException { + streamFormat = formats[0]; if (decoder != null) { - decoder.release(); - nextInputBuffer = null; + decoderReplacementState = REPLACEMENT_STATE_SIGNAL_END_OF_STREAM; + } else { + decoder = decoderFactory.createDecoder(streamFormat); } - decoder = decoderFactory.createDecoder(formats[0]); } @Override protected void onPositionReset(long positionUs, boolean joining) { clearOutput(); - resetBuffers(); - decoder.flush(); inputStreamEnded = false; outputStreamEnded = false; + if (decoderReplacementState != REPLACEMENT_STATE_NONE) { + replaceDecoder(); + } else { + releaseBuffers(); + decoder.flush(); + } } @Override @@ -155,13 +186,12 @@ public final class TextRenderer extends BaseRenderer implements Callback { if (nextSubtitle != null) { if (nextSubtitle.isEndOfStream()) { if (!textRendererNeedsUpdate && getNextEventTime() == Long.MAX_VALUE) { - if (subtitle != null) { - subtitle.release(); - subtitle = null; + if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) { + replaceDecoder(); + } else { + releaseBuffers(); + outputStreamEnded = true; } - nextSubtitle.release(); - nextSubtitle = null; - outputStreamEnded = true; } } else if (nextSubtitle.timeUs <= positionUs) { // Advance to the next subtitle. Sync the next event index and trigger an update. @@ -180,6 +210,10 @@ public final class TextRenderer extends BaseRenderer implements Callback { updateOutput(subtitle.getCues(positionUs)); } + if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) { + return; + } + try { while (!inputStreamEnded) { if (nextInputBuffer == null) { @@ -188,6 +222,13 @@ public final class TextRenderer extends BaseRenderer implements Callback { return; } } + if (decoderReplacementState == REPLACEMENT_STATE_SIGNAL_END_OF_STREAM) { + nextInputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + decoder.queueInputBuffer(nextInputBuffer); + nextInputBuffer = null; + decoderReplacementState = REPLACEMENT_STATE_WAIT_END_OF_STREAM; + return; + } // Try and read the next subtitle from the source. int result = readSource(formatHolder, nextInputBuffer, false); if (result == C.RESULT_BUFFER_READ) { @@ -200,7 +241,7 @@ public final class TextRenderer extends BaseRenderer implements Callback { decoder.queueInputBuffer(nextInputBuffer); nextInputBuffer = null; } else if (result == C.RESULT_NOTHING_READ) { - break; + return; } } } catch (SubtitleDecoderException e) { @@ -210,10 +251,9 @@ public final class TextRenderer extends BaseRenderer implements Callback { @Override protected void onDisabled() { + streamFormat = null; clearOutput(); - resetBuffers(); - decoder.release(); - decoder = null; + releaseDecoder(); super.onDisabled(); } @@ -229,7 +269,7 @@ public final class TextRenderer extends BaseRenderer implements Callback { return true; } - private void resetBuffers() { + private void releaseBuffers() { nextInputBuffer = null; nextSubtitleEventIndex = C.INDEX_UNSET; if (subtitle != null) { @@ -242,6 +282,18 @@ public final class TextRenderer extends BaseRenderer implements Callback { } } + private void releaseDecoder() { + releaseBuffers(); + decoder.release(); + decoder = null; + decoderReplacementState = REPLACEMENT_STATE_NONE; + } + + private void replaceDecoder() { + releaseDecoder(); + decoder = decoderFactory.createDecoder(streamFormat); + } + private long getNextEventTime() { return ((nextSubtitleEventIndex == C.INDEX_UNSET) || (nextSubtitleEventIndex >= subtitle.getEventTimeCount())) ? Long.MAX_VALUE @@ -267,8 +319,9 @@ public final class TextRenderer extends BaseRenderer implements Callback { case MSG_UPDATE_OUTPUT: invokeUpdateOutputInternal((List) msg.obj); return true; + default: + throw new IllegalStateException(); } - return false; } private void invokeUpdateOutputInternal(List cues) { diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java similarity index 95% rename from library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java index e63d1d4118..0a3f36fa87 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text.cea; +import android.support.annotation.NonNull; import android.text.Layout.Alignment; import com.google.android.exoplayer2.text.Cue; @@ -55,7 +56,7 @@ import com.google.android.exoplayer2.text.Cue; } @Override - public int compareTo(Cea708Cue other) { + public int compareTo(@NonNull Cea708Cue other) { if (other.priority < priority) { return -1; } else if (other.priority > priority) { diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaOutputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaOutputBuffer.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/text/cea/CeaOutputBuffer.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaOutputBuffer.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaSubtitle.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/text/cea/CeaSubtitle.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaSubtitle.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java new file mode 100644 index 0000000000..dbdc0434a1 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java @@ -0,0 +1,50 @@ +/* + * 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.google.android.exoplayer2.text.dvb; + +import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.List; + +/** + * A {@link SimpleSubtitleDecoder} for DVB Subtitles. + */ +public final class DvbDecoder extends SimpleSubtitleDecoder { + + private final DvbParser parser; + + /** + * @param initializationData The initialization data for the decoder. The initialization data + * must consist of a single byte array containing 5 bytes: flag_pes_stripped (1), + * composition_page (2), ancillary_page (2). + */ + public DvbDecoder(List initializationData) { + super("DvbDecoder"); + ParsableByteArray data = new ParsableByteArray(initializationData.get(0)); + int subtitleCompositionPage = data.readUnsignedShort(); + int subtitleAncillaryPage = data.readUnsignedShort(); + parser = new DvbParser(subtitleCompositionPage, subtitleAncillaryPage); + } + + @Override + protected DvbSubtitle decode(byte[] data, int length, boolean reset) { + if (reset) { + parser.reset(); + } + return new DvbSubtitle(parser.decode(data, length)); + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java new file mode 100644 index 0000000000..96c8a89801 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java @@ -0,0 +1,1025 @@ +/* + * 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.google.android.exoplayer2.text.dvb; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Region; +import android.util.Log; +import android.util.SparseArray; +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.util.ParsableBitArray; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Parses {@link Cue}s from a DVB subtitle bitstream. + */ +/* package */ final class DvbParser { + + private static final String TAG = "DvbParser"; + + // Segment types, as defined by ETSI EN 300 743 Table 2 + private static final int SEGMENT_TYPE_PAGE_COMPOSITION = 0x10; + private static final int SEGMENT_TYPE_REGION_COMPOSITION = 0x11; + private static final int SEGMENT_TYPE_CLUT_DEFINITION = 0x12; + private static final int SEGMENT_TYPE_OBJECT_DATA = 0x13; + private static final int SEGMENT_TYPE_DISPLAY_DEFINITION = 0x14; + + // Page states, as defined by ETSI EN 300 743 Table 3 + private static final int PAGE_STATE_NORMAL = 0; // Update. Only changed elements. + // private static final int PAGE_STATE_ACQUISITION = 1; // Refresh. All elements. + // private static final int PAGE_STATE_CHANGE = 2; // New. All elements. + + // Region depths, as defined by ETSI EN 300 743 Table 5 + // private static final int REGION_DEPTH_2_BIT = 1; + private static final int REGION_DEPTH_4_BIT = 2; + private static final int REGION_DEPTH_8_BIT = 3; + + // Object codings, as defined by ETSI EN 300 743 Table 8 + private static final int OBJECT_CODING_PIXELS = 0; + private static final int OBJECT_CODING_STRING = 1; + + // Pixel-data types, as defined by ETSI EN 300 743 Table 9 + private static final int DATA_TYPE_2BP_CODE_STRING = 0x10; + private static final int DATA_TYPE_4BP_CODE_STRING = 0x11; + private static final int DATA_TYPE_8BP_CODE_STRING = 0x12; + private static final int DATA_TYPE_24_TABLE_DATA = 0x20; + private static final int DATA_TYPE_28_TABLE_DATA = 0x21; + private static final int DATA_TYPE_48_TABLE_DATA = 0x22; + private static final int DATA_TYPE_END_LINE = 0xF0; + + // Clut mapping tables, as defined by ETSI EN 300 743 10.4, 10.5, 10.6 + private static final byte[] defaultMap2To4 = { + (byte) 0x00, (byte) 0x07, (byte) 0x08, (byte) 0x0F}; + private static final byte[] defaultMap2To8 = { + (byte) 0x00, (byte) 0x77, (byte) 0x88, (byte) 0xFF}; + private static final byte[] defaultMap4To8 = { + (byte) 0x00, (byte) 0x11, (byte) 0x22, (byte) 0x33, + (byte) 0x44, (byte) 0x55, (byte) 0x66, (byte) 0x77, + (byte) 0x88, (byte) 0x99, (byte) 0xAA, (byte) 0xBB, + (byte) 0xCC, (byte) 0xDD, (byte) 0xEE, (byte) 0xFF}; + + private final Paint defaultPaint; + private final Paint fillRegionPaint; + private final Canvas canvas; + private final DisplayDefinition defaultDisplayDefinition; + private final ClutDefinition defaultClutDefinition; + private final SubtitleService subtitleService; + + private Bitmap bitmap; + + /** + * Construct an instance for the given subtitle and ancillary page ids. + * + * @param subtitlePageId The id of the subtitle page carrying the subtitle to be parsed. + * @param ancillaryPageId The id of the ancillary page containing additional data. + */ + public DvbParser(int subtitlePageId, int ancillaryPageId) { + defaultPaint = new Paint(); + defaultPaint.setStyle(Paint.Style.FILL_AND_STROKE); + defaultPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); + defaultPaint.setPathEffect(null); + fillRegionPaint = new Paint(); + fillRegionPaint.setStyle(Paint.Style.FILL); + fillRegionPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OVER)); + fillRegionPaint.setPathEffect(null); + canvas = new Canvas(); + defaultDisplayDefinition = new DisplayDefinition(719, 575, 0, 719, 0, 575); + defaultClutDefinition = new ClutDefinition(0, generateDefault2BitClutEntries(), + generateDefault4BitClutEntries(), generateDefault8BitClutEntries()); + subtitleService = new SubtitleService(subtitlePageId, ancillaryPageId); + } + + /** + * Resets the parser. + */ + public void reset() { + subtitleService.reset(); + } + + /** + * Decodes a subtitling packet, returning a list of parsed {@link Cue}s. + * + * @param data The subtitling packet data to decode. + * @param limit The limit in {@code data} at which to stop decoding. + * @return The parsed {@link Cue}s. + */ + public List decode(byte[] data, int limit) { + // Parse the input data. + ParsableBitArray dataBitArray = new ParsableBitArray(data, limit); + while (dataBitArray.bitsLeft() >= 48 // sync_byte (8) + segment header (40) + && dataBitArray.readBits(8) == 0x0F) { + parseSubtitlingSegment(dataBitArray, subtitleService); + } + + if (subtitleService.pageComposition == null) { + return Collections.emptyList(); + } + + // Update the canvas bitmap if necessary. + DisplayDefinition displayDefinition = subtitleService.displayDefinition != null + ? subtitleService.displayDefinition : defaultDisplayDefinition; + if (bitmap == null || displayDefinition.width + 1 != bitmap.getWidth() + || displayDefinition.height + 1 != bitmap.getHeight()) { + bitmap = Bitmap.createBitmap(displayDefinition.width + 1, displayDefinition.height + 1, + Bitmap.Config.ARGB_8888); + canvas.setBitmap(bitmap); + } + + // Build the cues. + List cues = new ArrayList<>(); + SparseArray pageRegions = subtitleService.pageComposition.regions; + for (int i = 0; i < pageRegions.size(); i++) { + PageRegion pageRegion = pageRegions.valueAt(i); + int regionId = pageRegions.keyAt(i); + RegionComposition regionComposition = subtitleService.regions.get(regionId); + + // Clip drawing to the current region and display definition window. + int baseHorizontalAddress = pageRegion.horizontalAddress + + displayDefinition.horizontalPositionMinimum; + int baseVerticalAddress = pageRegion.verticalAddress + + displayDefinition.verticalPositionMinimum; + int clipRight = Math.min(baseHorizontalAddress + regionComposition.width, + displayDefinition.horizontalPositionMaximum); + int clipBottom = Math.min(baseVerticalAddress + regionComposition.height, + displayDefinition.verticalPositionMaximum); + canvas.clipRect(baseHorizontalAddress, baseVerticalAddress, clipRight, clipBottom, + Region.Op.REPLACE); + + ClutDefinition clutDefinition = subtitleService.cluts.get(regionComposition.clutId); + if (clutDefinition == null) { + clutDefinition = subtitleService.ancillaryCluts.get(regionComposition.clutId); + if (clutDefinition == null) { + clutDefinition = defaultClutDefinition; + } + } + + SparseArray regionObjects = regionComposition.regionObjects; + for (int j = 0; j < regionObjects.size(); j++) { + int objectId = regionObjects.keyAt(j); + RegionObject regionObject = regionObjects.valueAt(j); + ObjectData objectData = subtitleService.objects.get(objectId); + if (objectData == null) { + objectData = subtitleService.ancillaryObjects.get(objectId); + } + if (objectData != null) { + Paint paint = objectData.nonModifyingColorFlag ? null : defaultPaint; + paintPixelDataSubBlocks(objectData, clutDefinition, regionComposition.depth, + baseHorizontalAddress + regionObject.horizontalPosition, + baseVerticalAddress + regionObject.verticalPosition, paint, canvas); + } + } + + if (regionComposition.fillFlag) { + int color; + if (regionComposition.depth == REGION_DEPTH_8_BIT) { + color = clutDefinition.clutEntries8Bit[regionComposition.pixelCode8Bit]; + } else if (regionComposition.depth == REGION_DEPTH_4_BIT) { + color = clutDefinition.clutEntries4Bit[regionComposition.pixelCode4Bit]; + } else { + color = clutDefinition.clutEntries2Bit[regionComposition.pixelCode2Bit]; + } + fillRegionPaint.setColor(color); + canvas.drawRect(baseHorizontalAddress, baseVerticalAddress, + baseHorizontalAddress + regionComposition.width, + baseVerticalAddress + regionComposition.height, + fillRegionPaint); + } + + Bitmap cueBitmap = Bitmap.createBitmap(bitmap, baseHorizontalAddress, baseVerticalAddress, + regionComposition.width, regionComposition.height); + cues.add(new Cue(cueBitmap, (float) baseHorizontalAddress / displayDefinition.width, + Cue.ANCHOR_TYPE_START, (float) baseVerticalAddress / displayDefinition.height, + Cue.ANCHOR_TYPE_START, (float) regionComposition.width / displayDefinition.width, + (float) regionComposition.height / displayDefinition.height)); + + canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + } + + return cues; + } + + // Static parsing. + + /** + * Parses a subtitling segment, as defined by ETSI EN 300 743 7.2 + *

+ * The {@link SubtitleService} is updated with the parsed segment data. + */ + private static void parseSubtitlingSegment(ParsableBitArray data, SubtitleService service) { + int segmentType = data.readBits(8); + int pageId = data.readBits(16); + int dataFieldLength = data.readBits(16); + int dataFieldLimit = data.getBytePosition() + dataFieldLength; + + if ((dataFieldLength * 8) > data.bitsLeft()) { + Log.w(TAG, "Data field length exceeds limit"); + // Skip to the very end. + data.skipBits(data.bitsLeft()); + return; + } + + switch (segmentType) { + case SEGMENT_TYPE_DISPLAY_DEFINITION: + if (pageId == service.subtitlePageId) { + service.displayDefinition = parseDisplayDefinition(data); + } + break; + case SEGMENT_TYPE_PAGE_COMPOSITION: + if (pageId == service.subtitlePageId) { + PageComposition current = service.pageComposition; + PageComposition pageComposition = parsePageComposition(data, dataFieldLength); + if (pageComposition.state != PAGE_STATE_NORMAL) { + service.pageComposition = pageComposition; + service.regions.clear(); + service.cluts.clear(); + service.objects.clear(); + } else if (current != null && current.version != pageComposition.version) { + service.pageComposition = pageComposition; + } + } + break; + case SEGMENT_TYPE_REGION_COMPOSITION: + PageComposition pageComposition = service.pageComposition; + if (pageId == service.subtitlePageId && pageComposition != null) { + RegionComposition regionComposition = parseRegionComposition(data, dataFieldLength); + if (pageComposition.state == PAGE_STATE_NORMAL) { + regionComposition.mergeFrom(service.regions.get(regionComposition.id)); + } + service.regions.put(regionComposition.id, regionComposition); + } + break; + case SEGMENT_TYPE_CLUT_DEFINITION: + if (pageId == service.subtitlePageId) { + ClutDefinition clutDefinition = parseClutDefinition(data, dataFieldLength); + service.cluts.put(clutDefinition.id, clutDefinition); + } else if (pageId == service.ancillaryPageId) { + ClutDefinition clutDefinition = parseClutDefinition(data, dataFieldLength); + service.ancillaryCluts.put(clutDefinition.id, clutDefinition); + } + break; + case SEGMENT_TYPE_OBJECT_DATA: + if (pageId == service.subtitlePageId) { + ObjectData objectData = parseObjectData(data); + service.objects.put(objectData.id, objectData); + } else if (pageId == service.ancillaryPageId) { + ObjectData objectData = parseObjectData(data); + service.ancillaryObjects.put(objectData.id, objectData); + } + break; + default: + // Do nothing. + break; + } + + // Skip to the next segment. + data.skipBytes(dataFieldLimit - data.getBytePosition()); + } + + /** + * Parses a display definition segment, as defined by ETSI EN 300 743 7.2.1. + */ + private static DisplayDefinition parseDisplayDefinition(ParsableBitArray data) { + data.skipBits(4); // dds_version_number (4). + boolean displayWindowFlag = data.readBit(); + data.skipBits(3); // Skip reserved. + int width = data.readBits(16); + int height = data.readBits(16); + + int horizontalPositionMinimum; + int horizontalPositionMaximum; + int verticalPositionMinimum; + int verticalPositionMaximum; + if (displayWindowFlag) { + horizontalPositionMinimum = data.readBits(16); + horizontalPositionMaximum = data.readBits(16); + verticalPositionMinimum = data.readBits(16); + verticalPositionMaximum = data.readBits(16); + } else { + horizontalPositionMinimum = 0; + horizontalPositionMaximum = width; + verticalPositionMinimum = 0; + verticalPositionMaximum = height; + } + + return new DisplayDefinition(width, height, horizontalPositionMinimum, + horizontalPositionMaximum, verticalPositionMinimum, verticalPositionMaximum); + } + + /** + * Parses a page composition segment, as defined by ETSI EN 300 743 7.2.2. + */ + private static PageComposition parsePageComposition(ParsableBitArray data, int length) { + int timeoutSecs = data.readBits(8); + int version = data.readBits(4); + int state = data.readBits(2); + data.skipBits(2); + int remainingLength = length - 2; + + SparseArray regions = new SparseArray<>(); + while (remainingLength > 0) { + int regionId = data.readBits(8); + data.skipBits(8); // Skip reserved. + int regionHorizontalAddress = data.readBits(16); + int regionVerticalAddress = data.readBits(16); + remainingLength -= 6; + regions.put(regionId, new PageRegion(regionHorizontalAddress, regionVerticalAddress)); + } + + return new PageComposition(timeoutSecs, version, state, regions); + } + + /** + * Parses a region composition segment, as defined by ETSI EN 300 743 7.2.3. + */ + private static RegionComposition parseRegionComposition(ParsableBitArray data, int length) { + int id = data.readBits(8); + data.skipBits(4); // Skip region_version_number + boolean fillFlag = data.readBit(); + data.skipBits(3); // Skip reserved. + int width = data.readBits(16); + int height = data.readBits(16); + int levelOfCompatibility = data.readBits(3); + int depth = data.readBits(3); + data.skipBits(2); // Skip reserved. + int clutId = data.readBits(8); + int pixelCode8Bit = data.readBits(8); + int pixelCode4Bit = data.readBits(4); + int pixelCode2Bit = data.readBits(2); + data.skipBits(2); // Skip reserved + int remainingLength = length - 10; + + SparseArray regionObjects = new SparseArray<>(); + while (remainingLength > 0) { + int objectId = data.readBits(16); + int objectType = data.readBits(2); + int objectProvider = data.readBits(2); + int objectHorizontalPosition = data.readBits(12); + data.skipBits(4); // Skip reserved. + int objectVerticalPosition = data.readBits(12); + remainingLength -= 6; + + int foregroundPixelCode = 0; + int backgroundPixelCode = 0; + if (objectType == 0x01 || objectType == 0x02) { // Only seems to affect to char subtitles. + foregroundPixelCode = data.readBits(8); + backgroundPixelCode = data.readBits(8); + remainingLength -= 2; + } + + regionObjects.put(objectId, new RegionObject(objectType, objectProvider, + objectHorizontalPosition, objectVerticalPosition, foregroundPixelCode, + backgroundPixelCode)); + } + + return new RegionComposition(id, fillFlag, width, height, levelOfCompatibility, depth, clutId, + pixelCode8Bit, pixelCode4Bit, pixelCode2Bit, regionObjects); + } + + /** + * Parses a CLUT definition segment, as defined by ETSI EN 300 743 7.2.4. + */ + private static ClutDefinition parseClutDefinition(ParsableBitArray data, int length) { + int clutId = data.readBits(8); + data.skipBits(8); // Skip clut_version_number (4), reserved (4) + int remainingLength = length - 2; + + int[] clutEntries2Bit = generateDefault2BitClutEntries(); + int[] clutEntries4Bit = generateDefault4BitClutEntries(); + int[] clutEntries8Bit = generateDefault8BitClutEntries(); + + while (remainingLength > 0) { + int entryId = data.readBits(8); + int entryFlags = data.readBits(8); + remainingLength -= 2; + + int[] clutEntries; + if ((entryFlags & 0x80) != 0) { + clutEntries = clutEntries2Bit; + } else if ((entryFlags & 0x40) != 0) { + clutEntries = clutEntries4Bit; + } else { + clutEntries = clutEntries8Bit; + } + + int y; + int cr; + int cb; + int t; + if ((entryFlags & 0x01) != 0) { + y = data.readBits(8); + cr = data.readBits(8); + cb = data.readBits(8); + t = data.readBits(8); + remainingLength -= 4; + } else { + y = data.readBits(6) << 2; + cr = data.readBits(4) << 4; + cb = data.readBits(4) << 4; + t = data.readBits(2) << 6; + remainingLength -= 2; + } + + if (y == 0x00) { + cr = 0x00; + cb = 0x00; + t = 0xFF; + } + + int a = (byte) (0xFF - (t & 0xFF)); + int r = (int) (y + (1.40200 * (cr - 128))); + int g = (int) (y - (0.34414 * (cb - 128)) - (0.71414 * (cr - 128))); + int b = (int) (y + (1.77200 * (cb - 128))); + clutEntries[entryId] = getColor(a, Util.constrainValue(r, 0, 255), + Util.constrainValue(g, 0, 255), Util.constrainValue(b, 0, 255)); + } + + return new ClutDefinition(clutId, clutEntries2Bit, clutEntries4Bit, clutEntries8Bit); + } + + /** + * Parses an object data segment, as defined by ETSI EN 300 743 7.2.5. + * + * @return The parsed object data. + */ + private static ObjectData parseObjectData(ParsableBitArray data) { + int objectId = data.readBits(16); + data.skipBits(4); // Skip object_version_number + int objectCodingMethod = data.readBits(2); + boolean nonModifyingColorFlag = data.readBit(); + data.skipBits(1); // Skip reserved. + + byte[] topFieldData = null; + byte[] bottomFieldData = null; + + if (objectCodingMethod == OBJECT_CODING_STRING) { + int numberOfCodes = data.readBits(8); + // TODO: Parse and use character_codes. + data.skipBits(numberOfCodes * 16); // Skip character_codes. + } else if (objectCodingMethod == OBJECT_CODING_PIXELS) { + int topFieldDataLength = data.readBits(16); + int bottomFieldDataLength = data.readBits(16); + if (topFieldDataLength > 0) { + topFieldData = new byte[topFieldDataLength]; + data.readBytes(topFieldData, 0, topFieldDataLength); + } + if (bottomFieldDataLength > 0) { + bottomFieldData = new byte[bottomFieldDataLength]; + data.readBytes(bottomFieldData, 0, bottomFieldDataLength); + } else { + bottomFieldData = topFieldData; + } + } + + return new ObjectData(objectId, nonModifyingColorFlag, topFieldData, bottomFieldData); + } + + private static int[] generateDefault2BitClutEntries() { + int[] entries = new int[4]; + entries[0] = 0x00000000; + entries[1] = 0xFFFFFFFF; + entries[2] = 0xFF000000; + entries[3] = 0xFF7F7F7F; + return entries; + } + + private static int[] generateDefault4BitClutEntries() { + int[] entries = new int[16]; + entries[0] = 0x00000000; + for (int i = 1; i < entries.length; i++) { + if (i < 8) { + entries[i] = getColor( + 0xFF, + ((i & 0x01) != 0 ? 0xFF : 0x00), + ((i & 0x02) != 0 ? 0xFF : 0x00), + ((i & 0x04) != 0 ? 0xFF : 0x00)); + } else { + entries[i] = getColor( + 0xFF, + ((i & 0x01) != 0 ? 0x7F : 0x00), + ((i & 0x02) != 0 ? 0x7F : 0x00), + ((i & 0x04) != 0 ? 0x7F : 0x00)); + } + } + return entries; + } + + private static int[] generateDefault8BitClutEntries() { + int[] entries = new int[256]; + entries[0] = 0x00000000; + for (int i = 0; i < entries.length; i++) { + if (i < 8) { + entries[i] = getColor( + 0x3F, + ((i & 0x01) != 0 ? 0xFF : 0x00), + ((i & 0x02) != 0 ? 0xFF : 0x00), + ((i & 0x04) != 0 ? 0xFF : 0x00)); + } else { + switch (i & 0x88) { + case 0x00: + entries[i] = getColor( + 0xFF, + (((i & 0x01) != 0 ? 0x55 : 0x00) + ((i & 0x10) != 0 ? 0xAA : 0x00)), + (((i & 0x02) != 0 ? 0x55 : 0x00) + ((i & 0x20) != 0 ? 0xAA : 0x00)), + (((i & 0x04) != 0 ? 0x55 : 0x00) + ((i & 0x40) != 0 ? 0xAA : 0x00))); + break; + case 0x08: + entries[i] = getColor( + 0x7F, + (((i & 0x01) != 0 ? 0x55 : 0x00) + ((i & 0x10) != 0 ? 0xAA : 0x00)), + (((i & 0x02) != 0 ? 0x55 : 0x00) + ((i & 0x20) != 0 ? 0xAA : 0x00)), + (((i & 0x04) != 0 ? 0x55 : 0x00) + ((i & 0x40) != 0 ? 0xAA : 0x00))); + break; + case 0x80: + entries[i] = getColor( + 0xFF, + (127 + ((i & 0x01) != 0 ? 0x2B : 0x00) + ((i & 0x10) != 0 ? 0x55 : 0x00)), + (127 + ((i & 0x02) != 0 ? 0x2B : 0x00) + ((i & 0x20) != 0 ? 0x55 : 0x00)), + (127 + ((i & 0x04) != 0 ? 0x2B : 0x00) + ((i & 0x40) != 0 ? 0x55 : 0x00))); + break; + case 0x88: + entries[i] = getColor( + 0xFF, + (((i & 0x01) != 0 ? 0x2B : 0x00) + ((i & 0x10) != 0 ? 0x55 : 0x00)), + (((i & 0x02) != 0 ? 0x2B : 0x00) + ((i & 0x20) != 0 ? 0x55 : 0x00)), + (((i & 0x04) != 0 ? 0x2B : 0x00) + ((i & 0x40) != 0 ? 0x55 : 0x00))); + break; + } + } + } + return entries; + } + + private static int getColor(int a, int r, int g, int b) { + return (a << 24) | (r << 16) | (g << 8) | b; + } + + // Static drawing. + + /** + * Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. + */ + private static void paintPixelDataSubBlocks(ObjectData objectData, ClutDefinition clutDefinition, + int regionDepth, int horizontalAddress, int verticalAddress, Paint paint, Canvas canvas) { + int[] clutEntries; + if (regionDepth == REGION_DEPTH_8_BIT) { + clutEntries = clutDefinition.clutEntries8Bit; + } else if (regionDepth == REGION_DEPTH_4_BIT) { + clutEntries = clutDefinition.clutEntries4Bit; + } else { + clutEntries = clutDefinition.clutEntries2Bit; + } + paintPixelDataSubBlock(objectData.topFieldData, clutEntries, regionDepth, horizontalAddress, + verticalAddress, paint, canvas); + paintPixelDataSubBlock(objectData.bottomFieldData, clutEntries, regionDepth, horizontalAddress, + verticalAddress + 1, paint, canvas); + } + + /** + * Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. + */ + private static void paintPixelDataSubBlock(byte[] pixelData, int[] clutEntries, int regionDepth, + int horizontalAddress, int verticalAddress, Paint paint, Canvas canvas) { + ParsableBitArray data = new ParsableBitArray(pixelData); + int column = horizontalAddress; + int line = verticalAddress; + byte[] clutMapTable2To4 = null; + byte[] clutMapTable2To8 = null; + byte[] clutMapTable4To8 = null; + + while (data.bitsLeft() != 0) { + int dataType = data.readBits(8); + switch (dataType) { + case DATA_TYPE_2BP_CODE_STRING: + byte[] clutMapTable2ToX; + if (regionDepth == REGION_DEPTH_8_BIT) { + clutMapTable2ToX = clutMapTable2To8 == null ? defaultMap2To8 : clutMapTable2To8; + } else if (regionDepth == REGION_DEPTH_4_BIT) { + clutMapTable2ToX = clutMapTable2To4 == null ? defaultMap2To4 : clutMapTable2To4; + } else { + clutMapTable2ToX = null; + } + column = paint2BitPixelCodeString(data, clutEntries, clutMapTable2ToX, column, line, + paint, canvas); + data.byteAlign(); + break; + case DATA_TYPE_4BP_CODE_STRING: + byte[] clutMapTable4ToX; + if (regionDepth == REGION_DEPTH_8_BIT) { + clutMapTable4ToX = clutMapTable4To8 == null ? defaultMap4To8 : clutMapTable4To8; + } else { + clutMapTable4ToX = null; + } + column = paint4BitPixelCodeString(data, clutEntries, clutMapTable4ToX, column, line, + paint, canvas); + data.byteAlign(); + break; + case DATA_TYPE_8BP_CODE_STRING: + column = paint8BitPixelCodeString(data, clutEntries, null, column, line, paint, canvas); + break; + case DATA_TYPE_24_TABLE_DATA: + clutMapTable2To4 = buildClutMapTable(4, 4, data); + break; + case DATA_TYPE_28_TABLE_DATA: + clutMapTable2To8 = buildClutMapTable(4, 8, data); + break; + case DATA_TYPE_48_TABLE_DATA: + clutMapTable2To8 = buildClutMapTable(16, 8, data); + break; + case DATA_TYPE_END_LINE: + column = horizontalAddress; + line += 2; + break; + default: + // Do nothing. + break; + } + } + } + + /** + * Paint a 2-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. + */ + private static int paint2BitPixelCodeString(ParsableBitArray data, int[] clutEntries, + byte[] clutMapTable, int column, int line, Paint paint, Canvas canvas) { + boolean endOfPixelCodeString = false; + do { + int runLength = 0; + int clutIndex = 0; + int peek = data.readBits(2); + if (!data.readBit()) { + runLength = 1; + clutIndex = peek; + } else if (data.readBit()) { + runLength = 3 + data.readBits(3); + clutIndex = data.readBits(2); + } else if (!data.readBit()) { + switch (data.readBits(2)) { + case 0x00: + endOfPixelCodeString = true; + break; + case 0x01: + runLength = 2; + break; + case 0x02: + runLength = 12 + data.readBits(4); + clutIndex = data.readBits(2); + break; + case 0x03: + runLength = 29 + data.readBits(8); + clutIndex = data.readBits(2); + break; + } + } + + if (runLength != 0 && paint != null) { + paint.setColor(clutEntries[clutMapTable != null ? clutMapTable[clutIndex] : clutIndex]); + canvas.drawRect(column, line, column + runLength, line + 1, paint); + } + + column += runLength; + } while (!endOfPixelCodeString); + + return column; + } + + /** + * Paint a 4-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. + */ + private static int paint4BitPixelCodeString(ParsableBitArray data, int[] clutEntries, + byte[] clutMapTable, int column, int line, Paint paint, Canvas canvas) { + boolean endOfPixelCodeString = false; + do { + int runLength = 0; + int clutIndex = 0; + int peek = data.readBits(4); + if (peek != 0x00) { + runLength = 1; + clutIndex = peek; + } else if (!data.readBit()) { + peek = data.readBits(3); + if (peek != 0x00) { + runLength = 2 + peek; + clutIndex = 0x00; + } else { + endOfPixelCodeString = true; + } + } else if (!data.readBit()) { + runLength = 4 + data.readBits(2); + clutIndex = data.readBits(4); + } else { + switch (data.readBits(2)) { + case 0x00: + runLength = 1; + break; + case 0x01: + runLength = 2; + break; + case 0x02: + runLength = 9 + data.readBits(4); + clutIndex = data.readBits(4); + break; + case 0x03: + runLength = 25 + data.readBits(8); + clutIndex = data.readBits(4); + break; + } + } + + if (runLength != 0 && paint != null) { + paint.setColor(clutEntries[clutMapTable != null ? clutMapTable[clutIndex] : clutIndex]); + canvas.drawRect(column, line, column + runLength, line + 1, paint); + } + + column += runLength; + } while (!endOfPixelCodeString); + + return column; + } + + /** + * Paint an 8-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. + */ + private static int paint8BitPixelCodeString(ParsableBitArray data, int[] clutEntries, + byte[] clutMapTable, int column, int line, Paint paint, Canvas canvas) { + boolean endOfPixelCodeString = false; + do { + int runLength = 0; + int clutIndex = 0; + int peek = data.readBits(8); + if (peek != 0x00) { + runLength = 1; + clutIndex = peek; + } else { + if (!data.readBit()) { + peek = data.readBits(7); + if (peek != 0x00) { + runLength = peek; + clutIndex = 0x00; + } else { + endOfPixelCodeString = true; + } + } else { + runLength = data.readBits(7); + clutIndex = data.readBits(8); + } + } + + if (runLength != 0 && paint != null) { + paint.setColor(clutEntries[clutMapTable != null ? clutMapTable[clutIndex] : clutIndex]); + canvas.drawRect(column, line, column + runLength, line + 1, paint); + } + column += runLength; + } while (!endOfPixelCodeString); + + return column; + } + + private static byte[] buildClutMapTable(int length, int bitsPerEntry, ParsableBitArray data) { + byte[] clutMapTable = new byte[length]; + for (int i = 0; i < length; i++) { + clutMapTable[i] = (byte) data.readBits(bitsPerEntry); + } + return clutMapTable; + } + + // Private inner classes. + + /** + * The subtitle service definition. + */ + private static final class SubtitleService { + + public final int subtitlePageId; + public final int ancillaryPageId; + + public final SparseArray regions = new SparseArray<>(); + public final SparseArray cluts = new SparseArray<>(); + public final SparseArray objects = new SparseArray<>(); + public final SparseArray ancillaryCluts = new SparseArray<>(); + public final SparseArray ancillaryObjects = new SparseArray<>(); + + public DisplayDefinition displayDefinition; + public PageComposition pageComposition; + + public SubtitleService(int subtitlePageId, int ancillaryPageId) { + this.subtitlePageId = subtitlePageId; + this.ancillaryPageId = ancillaryPageId; + } + + public void reset() { + regions.clear(); + cluts.clear(); + objects.clear(); + ancillaryCluts.clear(); + ancillaryObjects.clear(); + displayDefinition = null; + pageComposition = null; + } + + } + + /** + * Contains the geometry and active area of the subtitle service. + *

+ * See ETSI EN 300 743 7.2.1 + */ + private static final class DisplayDefinition { + + public final int width; + public final int height; + + public final int horizontalPositionMinimum; + public final int horizontalPositionMaximum; + public final int verticalPositionMinimum; + public final int verticalPositionMaximum; + + public DisplayDefinition(int width, int height, int horizontalPositionMinimum, + int horizontalPositionMaximum, int verticalPositionMinimum, int verticalPositionMaximum) { + this.width = width; + this.height = height; + this.horizontalPositionMinimum = horizontalPositionMinimum; + this.horizontalPositionMaximum = horizontalPositionMaximum; + this.verticalPositionMinimum = verticalPositionMinimum; + this.verticalPositionMaximum = verticalPositionMaximum; + } + + } + + /** + * The page is the definition and arrangement of regions in the screen. + *

+ * See ETSI EN 300 743 7.2.2 + */ + private static final class PageComposition { + + public final int timeOutSecs; // TODO: Use this or remove it. + public final int version; + public final int state; + public final SparseArray regions; + + public PageComposition(int timeoutSecs, int version, int state, + SparseArray regions) { + this.timeOutSecs = timeoutSecs; + this.version = version; + this.state = state; + this.regions = regions; + } + + } + + /** + * A region within a {@link PageComposition}. + *

+ * See ETSI EN 300 743 7.2.2 + */ + private static final class PageRegion { + + public final int horizontalAddress; + public final int verticalAddress; + + public PageRegion(int horizontalAddress, int verticalAddress) { + this.horizontalAddress = horizontalAddress; + this.verticalAddress = verticalAddress; + } + + } + + /** + * An area of the page composed of a list of objects and a CLUT. + *

+ * See ETSI EN 300 743 7.2.3 + */ + private static final class RegionComposition { + + public final int id; + public final boolean fillFlag; + public final int width; + public final int height; + public final int levelOfCompatibility; // TODO: Use this or remove it. + public final int depth; + public final int clutId; + public final int pixelCode8Bit; + public final int pixelCode4Bit; + public final int pixelCode2Bit; + public final SparseArray regionObjects; + + public RegionComposition(int id, boolean fillFlag, int width, int height, + int levelOfCompatibility, int depth, int clutId, int pixelCode8Bit, int pixelCode4Bit, + int pixelCode2Bit, SparseArray regionObjects) { + this.id = id; + this.fillFlag = fillFlag; + this.width = width; + this.height = height; + this.levelOfCompatibility = levelOfCompatibility; + this.depth = depth; + this.clutId = clutId; + this.pixelCode8Bit = pixelCode8Bit; + this.pixelCode4Bit = pixelCode4Bit; + this.pixelCode2Bit = pixelCode2Bit; + this.regionObjects = regionObjects; + } + + public void mergeFrom(RegionComposition otherRegionComposition) { + if (otherRegionComposition == null) { + return; + } + SparseArray otherRegionObjects = otherRegionComposition.regionObjects; + for (int i = 0; i < otherRegionObjects.size(); i++) { + regionObjects.put(otherRegionObjects.keyAt(i), otherRegionObjects.valueAt(i)); + } + } + + } + + /** + * An object within a {@link RegionComposition}. + *

+ * See ETSI EN 300 743 7.2.3 + */ + private static final class RegionObject { + + public final int type; // TODO: Use this or remove it. + public final int provider; // TODO: Use this or remove it. + public final int horizontalPosition; + public final int verticalPosition; + public final int foregroundPixelCode; // TODO: Use this or remove it. + public final int backgroundPixelCode; // TODO: Use this or remove it. + + public RegionObject(int type, int provider, int horizontalPosition, + int verticalPosition, int foregroundPixelCode, int backgroundPixelCode) { + this.type = type; + this.provider = provider; + this.horizontalPosition = horizontalPosition; + this.verticalPosition = verticalPosition; + this.foregroundPixelCode = foregroundPixelCode; + this.backgroundPixelCode = backgroundPixelCode; + } + + } + + /** + * CLUT family definition containing the color tables for the three bit depths defined + *

+ * See ETSI EN 300 743 7.2.4 + */ + private static final class ClutDefinition { + + public final int id; + public final int[] clutEntries2Bit; + public final int[] clutEntries4Bit; + public final int[] clutEntries8Bit; + + public ClutDefinition(int id, int[] clutEntries2Bit, int[] clutEntries4Bit, + int[] clutEntries8bit) { + this.id = id; + this.clutEntries2Bit = clutEntries2Bit; + this.clutEntries4Bit = clutEntries4Bit; + this.clutEntries8Bit = clutEntries8bit; + } + + } + + /** + * The textual or graphical representation of an object. + *

+ * See ETSI EN 300 743 7.2.5 + */ + private static final class ObjectData { + + public final int id; + public final boolean nonModifyingColorFlag; + public final byte[] topFieldData; + public final byte[] bottomFieldData; + + public ObjectData(int id, boolean nonModifyingColorFlag, byte[] topFieldData, + byte[] bottomFieldData) { + this.id = id; + this.nonModifyingColorFlag = nonModifyingColorFlag; + this.topFieldData = topFieldData; + this.bottomFieldData = bottomFieldData; + } + + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbSubtitle.java new file mode 100644 index 0000000000..75728359c7 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbSubtitle.java @@ -0,0 +1,54 @@ +/* + * 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.google.android.exoplayer2.text.dvb; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.Subtitle; +import java.util.List; + +/** + * A representation of a DVB subtitle. + */ +/* package */ final class DvbSubtitle implements Subtitle { + + private final List cues; + + public DvbSubtitle(List cues) { + this.cues = cues; + } + + @Override + public int getNextEventTimeIndex(long timeUs) { + return C.INDEX_UNSET; + } + + @Override + public int getEventTimeCount() { + return 1; + } + + @Override + public long getEventTime(int index) { + return 0; + } + + @Override + public List getCues(long timeUs) { + return cues; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java similarity index 98% rename from library/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java index a848022ba9..e76f0fd7e2 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java @@ -46,7 +46,7 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { } @Override - protected SubripSubtitle decode(byte[] bytes, int length) { + protected SubripSubtitle decode(byte[] bytes, int length, boolean reset) { ArrayList cues = new ArrayList<>(); LongArray cueTimesUs = new LongArray(); ParsableByteArray subripData = new ParsableByteArray(bytes, length); diff --git a/library/src/main/java/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java similarity index 99% rename from library/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java index 34ff757e2f..71ce17eeed 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java @@ -94,7 +94,8 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { } @Override - protected TtmlSubtitle decode(byte[] bytes, int length) throws SubtitleDecoderException { + protected TtmlSubtitle decode(byte[] bytes, int length, boolean reset) + throws SubtitleDecoderException { try { XmlPullParser xmlParser = xmlParserFactory.newPullParser(); Map globalStyles = new HashMap<>(); diff --git a/library/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java new file mode 100644 index 0000000000..2270ccc632 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java @@ -0,0 +1,234 @@ +/* + * 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.google.android.exoplayer2.text.tx3g; + +import android.graphics.Color; +import android.graphics.Typeface; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; +import android.text.style.TypefaceSpan; +import android.text.style.UnderlineSpan; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import com.google.android.exoplayer2.text.Subtitle; +import com.google.android.exoplayer2.text.SubtitleDecoderException; +import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; +import java.nio.charset.Charset; +import java.util.List; + +/** + * A {@link SimpleSubtitleDecoder} for tx3g. + *

+ * Currently supports parsing of a single text track with embedded styles. + */ +public final class Tx3gDecoder extends SimpleSubtitleDecoder { + + private static final char BOM_UTF16_BE = '\uFEFF'; + private static final char BOM_UTF16_LE = '\uFFFE'; + + private static final int TYPE_STYL = Util.getIntegerCodeForString("styl"); + private static final int TYPE_TBOX = Util.getIntegerCodeForString("tbox"); + private static final String TX3G_SERIF = "Serif"; + + private static final int SIZE_ATOM_HEADER = 8; + private static final int SIZE_SHORT = 2; + private static final int SIZE_BOM_UTF16 = 2; + private static final int SIZE_STYLE_RECORD = 12; + + private static final int FONT_FACE_BOLD = 0x0001; + private static final int FONT_FACE_ITALIC = 0x0002; + private static final int FONT_FACE_UNDERLINE = 0x0004; + + private static final int SPAN_PRIORITY_LOW = (0xFF << Spanned.SPAN_PRIORITY_SHIFT); + private static final int SPAN_PRIORITY_HIGH = (0x00 << Spanned.SPAN_PRIORITY_SHIFT); + + private static final int DEFAULT_FONT_FACE = 0; + private static final int DEFAULT_COLOR = Color.WHITE; + private static final String DEFAULT_FONT_FAMILY = C.SANS_SERIF_NAME; + private static final float DEFAULT_VERTICAL_PLACEMENT = 0.85f; + + private final ParsableByteArray parsableByteArray; + private boolean customVerticalPlacement; + private int defaultFontFace; + private int defaultColorRgba; + private String defaultFontFamily; + private float defaultVerticalPlacement; + private int calculatedVideoTrackHeight; + + /** + * Sets up a new {@link Tx3gDecoder} with default values. + * + * @param initializationData Sample description atom ('stsd') data with default subtitle styles. + */ + public Tx3gDecoder(List initializationData) { + super("Tx3gDecoder"); + parsableByteArray = new ParsableByteArray(); + decodeInitializationData(initializationData); + } + + private void decodeInitializationData(List initializationData) { + if (initializationData != null && initializationData.size() == 1 + && (initializationData.get(0).length == 48 || initializationData.get(0).length == 53)) { + byte[] initializationBytes = initializationData.get(0); + defaultFontFace = initializationBytes[24]; + defaultColorRgba = ((initializationBytes[26] & 0xFF) << 24) + | ((initializationBytes[27] & 0xFF) << 16) + | ((initializationBytes[28] & 0xFF) << 8) + | (initializationBytes[29] & 0xFF); + String fontFamily = new String(initializationBytes, 43, initializationBytes.length - 43); + defaultFontFamily = TX3G_SERIF.equals(fontFamily) ? C.SERIF_NAME : C.SANS_SERIF_NAME; + //font size (initializationBytes[25]) is 5% of video height + calculatedVideoTrackHeight = 20 * initializationBytes[25]; + customVerticalPlacement = (initializationBytes[0] & 0x20) != 0; + if (customVerticalPlacement) { + int requestedVerticalPlacement = ((initializationBytes[10] & 0xFF) << 8) + | (initializationBytes[11] & 0xFF); + defaultVerticalPlacement = (float) requestedVerticalPlacement / calculatedVideoTrackHeight; + defaultVerticalPlacement = Util.constrainValue(defaultVerticalPlacement, 0.0f, 0.95f); + } else { + defaultVerticalPlacement = DEFAULT_VERTICAL_PLACEMENT; + } + } else { + defaultFontFace = DEFAULT_FONT_FACE; + defaultColorRgba = DEFAULT_COLOR; + defaultFontFamily = DEFAULT_FONT_FAMILY; + customVerticalPlacement = false; + defaultVerticalPlacement = DEFAULT_VERTICAL_PLACEMENT; + } + } + + @Override + protected Subtitle decode(byte[] bytes, int length, boolean reset) + throws SubtitleDecoderException { + parsableByteArray.reset(bytes, length); + String cueTextString = readSubtitleText(parsableByteArray); + if (cueTextString.isEmpty()) { + return Tx3gSubtitle.EMPTY; + } + // Attach default styles. + SpannableStringBuilder cueText = new SpannableStringBuilder(cueTextString); + attachFontFace(cueText, defaultFontFace, DEFAULT_FONT_FACE, 0, cueText.length(), + SPAN_PRIORITY_LOW); + attachColor(cueText, defaultColorRgba, DEFAULT_COLOR, 0, cueText.length(), + SPAN_PRIORITY_LOW); + attachFontFamily(cueText, defaultFontFamily, DEFAULT_FONT_FAMILY, 0, cueText.length(), + SPAN_PRIORITY_LOW); + float verticalPlacement = defaultVerticalPlacement; + // Find and attach additional styles. + while (parsableByteArray.bytesLeft() >= SIZE_ATOM_HEADER) { + int position = parsableByteArray.getPosition(); + int atomSize = parsableByteArray.readInt(); + int atomType = parsableByteArray.readInt(); + if (atomType == TYPE_STYL) { + assertTrue(parsableByteArray.bytesLeft() >= SIZE_SHORT); + int styleRecordCount = parsableByteArray.readUnsignedShort(); + for (int i = 0; i < styleRecordCount; i++) { + applyStyleRecord(parsableByteArray, cueText); + } + } else if (atomType == TYPE_TBOX && customVerticalPlacement) { + assertTrue(parsableByteArray.bytesLeft() >= SIZE_SHORT); + int requestedVerticalPlacement = parsableByteArray.readUnsignedShort(); + verticalPlacement = (float) requestedVerticalPlacement / calculatedVideoTrackHeight; + verticalPlacement = Util.constrainValue(verticalPlacement, 0.0f, 0.95f); + } + parsableByteArray.setPosition(position + atomSize); + } + return new Tx3gSubtitle(new Cue(cueText, null, verticalPlacement, Cue.LINE_TYPE_FRACTION, + Cue.ANCHOR_TYPE_START, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET)); + } + + private static String readSubtitleText(ParsableByteArray parsableByteArray) + throws SubtitleDecoderException { + assertTrue(parsableByteArray.bytesLeft() >= SIZE_SHORT); + int textLength = parsableByteArray.readUnsignedShort(); + if (textLength == 0) { + return ""; + } + if (parsableByteArray.bytesLeft() >= SIZE_BOM_UTF16) { + char firstChar = parsableByteArray.peekChar(); + if (firstChar == BOM_UTF16_BE || firstChar == BOM_UTF16_LE) { + return parsableByteArray.readString(textLength, Charset.forName(C.UTF16_NAME)); + } + } + return parsableByteArray.readString(textLength, Charset.forName(C.UTF8_NAME)); + } + + private void applyStyleRecord(ParsableByteArray parsableByteArray, + SpannableStringBuilder cueText) throws SubtitleDecoderException { + assertTrue(parsableByteArray.bytesLeft() >= SIZE_STYLE_RECORD); + int start = parsableByteArray.readUnsignedShort(); + int end = parsableByteArray.readUnsignedShort(); + parsableByteArray.skipBytes(2); // font identifier + int fontFace = parsableByteArray.readUnsignedByte(); + parsableByteArray.skipBytes(1); // font size + int colorRgba = parsableByteArray.readInt(); + attachFontFace(cueText, fontFace, defaultFontFace, start, end, SPAN_PRIORITY_HIGH); + attachColor(cueText, colorRgba, defaultColorRgba, start, end, SPAN_PRIORITY_HIGH); + } + + private static void attachFontFace(SpannableStringBuilder cueText, int fontFace, + int defaultFontFace, int start, int end, int spanPriority) { + if (fontFace != defaultFontFace) { + final int flags = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | spanPriority; + boolean isBold = (fontFace & FONT_FACE_BOLD) != 0; + boolean isItalic = (fontFace & FONT_FACE_ITALIC) != 0; + if (isBold) { + if (isItalic) { + cueText.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), start, end, flags); + } else { + cueText.setSpan(new StyleSpan(Typeface.BOLD), start, end, flags); + } + } else if (isItalic) { + cueText.setSpan(new StyleSpan(Typeface.ITALIC), start, end, flags); + } + boolean isUnderlined = (fontFace & FONT_FACE_UNDERLINE) != 0; + if (isUnderlined) { + cueText.setSpan(new UnderlineSpan(), start, end, flags); + } + if (!isUnderlined && !isBold && !isItalic) { + cueText.setSpan(new StyleSpan(Typeface.NORMAL), start, end, flags); + } + } + } + + private static void attachColor(SpannableStringBuilder cueText, int colorRgba, + int defaultColorRgba, int start, int end, int spanPriority) { + if (colorRgba != defaultColorRgba) { + int colorArgb = ((colorRgba & 0xFF) << 24) | (colorRgba >>> 8); + cueText.setSpan(new ForegroundColorSpan(colorArgb), start, end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | spanPriority); + } + } + + @SuppressWarnings("ReferenceEquality") + private static void attachFontFamily(SpannableStringBuilder cueText, String fontFamily, + String defaultFontFamily, int start, int end, int spanPriority) { + if (fontFamily != defaultFontFamily) { + cueText.setSpan(new TypefaceSpan(fontFamily), start, end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | spanPriority); + } + } + + private static void assertTrue(boolean checkValue) throws SubtitleDecoderException { + if (!checkValue) { + throw new SubtitleDecoderException("Unexpected subtitle format."); + } + } +} diff --git a/library/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gSubtitle.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gSubtitle.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gSubtitle.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java similarity index 96% rename from library/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java index 916e67128a..159dd4f2e0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java @@ -45,7 +45,8 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder { } @Override - protected Mp4WebvttSubtitle decode(byte[] bytes, int length) throws SubtitleDecoderException { + protected Mp4WebvttSubtitle decode(byte[] bytes, int length, boolean reset) + throws SubtitleDecoderException { // Webvtt in Mp4 samples have boxes inside of them, so we have to do a traditional box parsing: // first 4 bytes size and then 4 bytes type. sampleData.reset(bytes, length); diff --git a/library/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttSubtitle.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttSubtitle.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttSubtitle.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java similarity index 99% rename from library/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java index 932d4a6bed..30c9c8737e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.text.webvtt; import android.graphics.Typeface; +import android.support.annotation.NonNull; import android.text.Layout.Alignment; import android.text.Spannable; import android.text.SpannableStringBuilder; @@ -476,7 +477,7 @@ import java.util.regex.Pattern; } @Override - public int compareTo(StyleMatch another) { + public int compareTo(@NonNull StyleMatch another) { return this.score - another.score; } diff --git a/library/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java similarity index 97% rename from library/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java index fb177dcea7..7c3262fbba 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java @@ -54,7 +54,8 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder { } @Override - protected WebvttSubtitle decode(byte[] bytes, int length) throws SubtitleDecoderException { + protected WebvttSubtitle decode(byte[] bytes, int length, boolean reset) + throws SubtitleDecoderException { parsableWebvttData.reset(bytes, length); // Initialization for consistent starting state. webvttCueBuilder.reset(); diff --git a/library/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java rename to library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java rename to library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java similarity index 84% rename from library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java rename to library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 1fa372ca0a..9db77fd7ad 100644 --- a/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -372,24 +372,24 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static final int[] NO_TRACKS = new int[0]; private static final int WITHIN_RENDERER_CAPABILITIES_BONUS = 1000; - private final TrackSelection.Factory adaptiveVideoTrackSelectionFactory; + private final TrackSelection.Factory adaptiveTrackSelectionFactory; private final AtomicReference paramsReference; /** - * Constructs an instance that does not support adaptive video. + * Constructs an instance that does not support adaptive tracks. */ public DefaultTrackSelector() { this(null); } /** - * Constructs an instance that uses a factory to create adaptive video track selections. + * Constructs an instance that uses a factory to create adaptive track selections. * - * @param adaptiveVideoTrackSelectionFactory A factory for adaptive video {@link TrackSelection}s, - * or null if the selector should not support adaptive video. + * @param adaptiveTrackSelectionFactory A factory for adaptive {@link TrackSelection}s, or null if + * the selector should not support adaptive tracks. */ - public DefaultTrackSelector(TrackSelection.Factory adaptiveVideoTrackSelectionFactory) { - this.adaptiveVideoTrackSelectionFactory = adaptiveVideoTrackSelectionFactory; + public DefaultTrackSelector(TrackSelection.Factory adaptiveTrackSelectionFactory) { + this.adaptiveTrackSelectionFactory = adaptiveTrackSelectionFactory; paramsReference = new AtomicReference<>(new Parameters()); } @@ -424,6 +424,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { int rendererCount = rendererCapabilities.length; TrackSelection[] rendererTrackSelections = new TrackSelection[rendererCount]; Parameters params = paramsReference.get(); + boolean videoTrackAndRendererPresent = false; for (int i = 0; i < rendererCount; i++) { if (C.TRACK_TYPE_VIDEO == rendererCapabilities[i].getTrackType()) { @@ -431,8 +432,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { rendererTrackGroupArrays[i], rendererFormatSupports[i], params.maxVideoWidth, params.maxVideoHeight, params.maxVideoBitrate, params.allowNonSeamlessAdaptiveness, params.allowMixedMimeAdaptiveness, params.viewportWidth, params.viewportHeight, - params.orientationMayChange, adaptiveVideoTrackSelectionFactory, + params.orientationMayChange, adaptiveTrackSelectionFactory, params.exceedVideoConstraintsIfNecessary, params.exceedRendererCapabilitiesIfNecessary); + videoTrackAndRendererPresent |= rendererTrackGroupArrays[i].length > 0; } } @@ -444,7 +446,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { case C.TRACK_TYPE_AUDIO: rendererTrackSelections[i] = selectAudioTrack(rendererTrackGroupArrays[i], rendererFormatSupports[i], params.preferredAudioLanguage, - params.exceedRendererCapabilitiesIfNecessary); + params.exceedRendererCapabilitiesIfNecessary, params.allowMixedMimeAdaptiveness, + videoTrackAndRendererPresent ? null : adaptiveTrackSelectionFactory); break; case C.TRACK_TYPE_TEXT: rendererTrackSelections[i] = selectTextTrack(rendererTrackGroupArrays[i], @@ -467,15 +470,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroupArray groups, int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, int maxVideoBitrate, boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness, int viewportWidth, int viewportHeight, boolean orientationMayChange, - TrackSelection.Factory adaptiveVideoTrackSelectionFactory, - boolean exceedConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary) - throws ExoPlaybackException { + TrackSelection.Factory adaptiveTrackSelectionFactory, boolean exceedConstraintsIfNecessary, + boolean exceedRendererCapabilitiesIfNecessary) throws ExoPlaybackException { TrackSelection selection = null; - if (adaptiveVideoTrackSelectionFactory != null) { + if (adaptiveTrackSelectionFactory != null) { selection = selectAdaptiveVideoTrack(rendererCapabilities, groups, formatSupport, maxVideoWidth, maxVideoHeight, maxVideoBitrate, allowNonSeamlessAdaptiveness, allowMixedMimeAdaptiveness, viewportWidth, viewportHeight, - orientationMayChange, adaptiveVideoTrackSelectionFactory); + orientationMayChange, adaptiveTrackSelectionFactory); } if (selection == null) { selection = selectFixedVideoTrack(groups, formatSupport, maxVideoWidth, maxVideoHeight, @@ -489,7 +491,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroupArray groups, int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, int maxVideoBitrate, boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness, int viewportWidth, int viewportHeight, boolean orientationMayChange, - TrackSelection.Factory adaptiveVideoTrackSelectionFactory) throws ExoPlaybackException { + TrackSelection.Factory adaptiveTrackSelectionFactory) throws ExoPlaybackException { int requiredAdaptiveSupport = allowNonSeamlessAdaptiveness ? (RendererCapabilities.ADAPTIVE_NOT_SEAMLESS | RendererCapabilities.ADAPTIVE_SEAMLESS) : RendererCapabilities.ADAPTIVE_SEAMLESS; @@ -497,17 +499,17 @@ public class DefaultTrackSelector extends MappingTrackSelector { && (rendererCapabilities.supportsMixedMimeTypeAdaptation() & requiredAdaptiveSupport) != 0; for (int i = 0; i < groups.length; i++) { TrackGroup group = groups.get(i); - int[] adaptiveTracks = getAdaptiveTracksForGroup(group, formatSupport[i], + int[] adaptiveTracks = getAdaptiveVideoTracksForGroup(group, formatSupport[i], allowMixedMimeTypes, requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight, maxVideoBitrate, viewportWidth, viewportHeight, orientationMayChange); if (adaptiveTracks.length > 0) { - return adaptiveVideoTrackSelectionFactory.createTrackSelection(group, adaptiveTracks); + return adaptiveTrackSelectionFactory.createTrackSelection(group, adaptiveTracks); } } return null; } - private static int[] getAdaptiveTracksForGroup(TrackGroup group, int[] formatSupport, + private static int[] getAdaptiveVideoTracksForGroup(TrackGroup group, int[] formatSupport, boolean allowMixedMimeTypes, int requiredAdaptiveSupport, int maxVideoWidth, int maxVideoHeight, int maxVideoBitrate, int viewportWidth, int viewportHeight, boolean orientationMayChange) { @@ -530,7 +532,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { int trackIndex = selectedTrackIndices.get(i); String sampleMimeType = group.getFormat(trackIndex).sampleMimeType; if (seenMimeTypes.add(sampleMimeType)) { - int countForMimeType = getAdaptiveTrackCountForMimeType(group, formatSupport, + int countForMimeType = getAdaptiveVideoTrackCountForMimeType(group, formatSupport, requiredAdaptiveSupport, sampleMimeType, maxVideoWidth, maxVideoHeight, maxVideoBitrate, selectedTrackIndices); if (countForMimeType > selectedMimeTypeTrackCount) { @@ -542,13 +544,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { } // Filter by the selected mime type. - filterAdaptiveTrackCountForMimeType(group, formatSupport, requiredAdaptiveSupport, + filterAdaptiveVideoTrackCountForMimeType(group, formatSupport, requiredAdaptiveSupport, selectedMimeType, maxVideoWidth, maxVideoHeight, maxVideoBitrate, selectedTrackIndices); return selectedTrackIndices.size() < 2 ? NO_TRACKS : Util.toArray(selectedTrackIndices); } - private static int getAdaptiveTrackCountForMimeType(TrackGroup group, int[] formatSupport, + private static int getAdaptiveVideoTrackCountForMimeType(TrackGroup group, int[] formatSupport, int requiredAdaptiveSupport, String mimeType, int maxVideoWidth, int maxVideoHeight, int maxVideoBitrate, List selectedTrackIndices) { int adaptiveTrackCount = 0; @@ -563,9 +565,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { return adaptiveTrackCount; } - private static void filterAdaptiveTrackCountForMimeType(TrackGroup group, int[] formatSupport, - int requiredAdaptiveSupport, String mimeType, int maxVideoWidth, int maxVideoHeight, - int maxVideoBitrate, List selectedTrackIndices) { + private static void filterAdaptiveVideoTrackCountForMimeType(TrackGroup group, + int[] formatSupport, int requiredAdaptiveSupport, String mimeType, int maxVideoWidth, + int maxVideoHeight, int maxVideoBitrate, List selectedTrackIndices) { for (int i = selectedTrackIndices.size() - 1; i >= 0; i--) { int trackIndex = selectedTrackIndices.get(i); if (!isSupportedAdaptiveVideoTrack(group.getFormat(trackIndex), mimeType, @@ -661,9 +663,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Audio track selection implementation. protected TrackSelection selectAudioTrack(TrackGroupArray groups, int[][] formatSupport, - String preferredAudioLanguage, boolean exceedRendererCapabilitiesIfNecessary) { - TrackGroup selectedGroup = null; - int selectedTrackIndex = 0; + String preferredAudioLanguage, boolean exceedRendererCapabilitiesIfNecessary, + boolean allowMixedMimeAdaptiveness, TrackSelection.Factory adaptiveTrackSelectionFactory) { + int selectedGroupIndex = C.INDEX_UNSET; + int selectedTrackIndex = C.INDEX_UNSET; int selectedTrackScore = 0; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); @@ -671,32 +674,105 @@ public class DefaultTrackSelector extends MappingTrackSelector { for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); - boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; - int trackScore; - if (formatHasLanguage(format, preferredAudioLanguage)) { - if (isDefault) { - trackScore = 4; - } else { - trackScore = 3; - } - } else if (isDefault) { - trackScore = 2; - } else { - trackScore = 1; - } - if (isSupported(trackFormatSupport[trackIndex], false)) { - trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS; - } + int trackScore = getAudioTrackScore(trackFormatSupport[trackIndex], + preferredAudioLanguage, format); if (trackScore > selectedTrackScore) { - selectedGroup = trackGroup; + selectedGroupIndex = groupIndex; selectedTrackIndex = trackIndex; selectedTrackScore = trackScore; } } } } - return selectedGroup == null ? null - : new FixedTrackSelection(selectedGroup, selectedTrackIndex); + + if (selectedGroupIndex == C.INDEX_UNSET) { + return null; + } + + TrackGroup selectedGroup = groups.get(selectedGroupIndex); + if (adaptiveTrackSelectionFactory != null) { + // If the group of the track with the highest score allows it, try to enable adaptation. + int[] adaptiveTracks = getAdaptiveAudioTracks(selectedGroup, + formatSupport[selectedGroupIndex], allowMixedMimeAdaptiveness); + if (adaptiveTracks.length > 0) { + return adaptiveTrackSelectionFactory.createTrackSelection(selectedGroup, + adaptiveTracks); + } + } + return new FixedTrackSelection(selectedGroup, selectedTrackIndex); + } + + private static int getAudioTrackScore(int formatSupport, String preferredLanguage, + Format format) { + boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; + int trackScore; + if (formatHasLanguage(format, preferredLanguage)) { + if (isDefault) { + trackScore = 4; + } else { + trackScore = 3; + } + } else if (isDefault) { + trackScore = 2; + } else { + trackScore = 1; + } + if (isSupported(formatSupport, false)) { + trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS; + } + return trackScore; + } + + private static int[] getAdaptiveAudioTracks(TrackGroup group, int[] formatSupport, + boolean allowMixedMimeTypes) { + int selectedConfigurationTrackCount = 0; + AudioConfigurationTuple selectedConfiguration = null; + HashSet seenConfigurationTuples = new HashSet<>(); + for (int i = 0; i < group.length; i++) { + Format format = group.getFormat(i); + AudioConfigurationTuple configuration = new AudioConfigurationTuple( + format.channelCount, format.sampleRate, + allowMixedMimeTypes ? null : format.sampleMimeType); + if (seenConfigurationTuples.add(configuration)) { + int configurationCount = getAdaptiveAudioTrackCount(group, formatSupport, configuration); + if (configurationCount > selectedConfigurationTrackCount) { + selectedConfiguration = configuration; + selectedConfigurationTrackCount = configurationCount; + } + } + } + + if (selectedConfigurationTrackCount > 1) { + int[] adaptiveIndices = new int[selectedConfigurationTrackCount]; + int index = 0; + for (int i = 0; i < group.length; i++) { + if (isSupportedAdaptiveAudioTrack(group.getFormat(i), formatSupport[i], + selectedConfiguration)) { + adaptiveIndices[index++] = i; + } + } + return adaptiveIndices; + } + return NO_TRACKS; + } + + private static int getAdaptiveAudioTrackCount(TrackGroup group, int[] formatSupport, + AudioConfigurationTuple configuration) { + int count = 0; + for (int i = 0; i < group.length; i++) { + if (isSupportedAdaptiveAudioTrack(group.getFormat(i), formatSupport[i], configuration)) { + count++; + } + } + return count; + } + + private static boolean isSupportedAdaptiveAudioTrack(Format format, int formatSupport, + AudioConfigurationTuple configuration) { + return isSupported(formatSupport, false) && format.channelCount == configuration.channelCount + && format.sampleRate == configuration.sampleRate + && (configuration.mimeType == null + || TextUtils.equals(configuration.mimeType, format.sampleMimeType)); } // Text track selection implementation. @@ -791,7 +867,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } protected static boolean formatHasLanguage(Format format, String language) { - return language != null && language.equals(Util.normalizeLanguageCode(format.language)); + return TextUtils.equals(language, Util.normalizeLanguageCode(format.language)); } // Viewport size util methods. @@ -865,4 +941,39 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } + private static final class AudioConfigurationTuple { + + public final int channelCount; + public final int sampleRate; + public final String mimeType; + + public AudioConfigurationTuple(int channelCount, int sampleRate, String mimeType) { + this.channelCount = channelCount; + this.sampleRate = sampleRate; + this.mimeType = mimeType; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + AudioConfigurationTuple other = (AudioConfigurationTuple) obj; + return channelCount == other.channelCount && sampleRate == other.sampleRate + && TextUtils.equals(mimeType, other.mimeType); + } + + @Override + public int hashCode() { + int result = channelCount; + result = 31 * result + sampleRate; + result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0); + return result; + } + + } + } diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java rename to library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java rename to library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java rename to library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java rename to library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java rename to library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java rename to library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java rename to library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/Allocation.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocation.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/Allocation.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocation.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/Allocator.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocator.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/Allocator.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocator.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/BandwidthMeter.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/BandwidthMeter.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/BandwidthMeter.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/BandwidthMeter.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DataSink.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSink.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/DataSink.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSink.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DataSourceException.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSourceException.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/DataSourceException.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSourceException.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DataSourceInputStream.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSourceInputStream.java similarity index 93% rename from library/src/main/java/com/google/android/exoplayer2/upstream/DataSourceInputStream.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSourceInputStream.java index c4296bd6f6..2f2075f354 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/DataSourceInputStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSourceInputStream.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.upstream; +import android.support.annotation.NonNull; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; @@ -71,12 +72,12 @@ public final class DataSourceInputStream extends InputStream { } @Override - public int read(byte[] buffer) throws IOException { + public int read(@NonNull byte[] buffer) throws IOException { return read(buffer, 0, buffer.length); } @Override - public int read(byte[] buffer, int offset, int length) throws IOException { + public int read(@NonNull byte[] buffer, int offset, int length) throws IOException { Assertions.checkState(!closed); checkOpened(); int bytesRead = dataSource.read(buffer, offset, length); diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultAllocator.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultAllocator.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/DefaultAllocator.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultAllocator.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java new file mode 100644 index 0000000000..c20868ef00 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java @@ -0,0 +1,58 @@ +/* + * 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.google.android.exoplayer2.upstream; + +import android.net.Uri; +import java.io.IOException; + +/** + * A dummy DataSource which provides no data. {@link #open(DataSpec)} throws {@link IOException}. + */ +public final class DummyDataSource implements DataSource { + + public static final DummyDataSource INSTANCE = new DummyDataSource(); + + /** A factory that that produces {@link DummyDataSource}. */ + public static final Factory FACTORY = new Factory() { + @Override + public DataSource createDataSource() { + return new DummyDataSource(); + } + }; + + private DummyDataSource() {} + + @Override + public long open(DataSpec dataSpec) throws IOException { + throw new IOException("Dummy source"); + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public Uri getUri() { + return null; + } + + @Override + public void close() throws IOException { + // do nothing. + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/Loader.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/Loader.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/LoaderErrorThrower.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/LoaderErrorThrower.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/LoaderErrorThrower.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/LoaderErrorThrower.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java similarity index 97% rename from library/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java index c25638ac86..adf245d9aa 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java @@ -59,7 +59,7 @@ public final class ParsingLoadable implements Loadable { public final int type; private final DataSource dataSource; - private final Parser parser; + private final Parser parser; private volatile T result; private volatile boolean isCanceled; @@ -71,7 +71,7 @@ public final class ParsingLoadable implements Loadable { * @param type See {@link #type}. * @param parser Parses the object from the response. */ - public ParsingLoadable(DataSource dataSource, Uri uri, int type, Parser parser) { + public ParsingLoadable(DataSource dataSource, Uri uri, int type, Parser parser) { this.dataSource = dataSource; this.dataSpec = new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP); this.type = type; diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSourceFactory.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSourceFactory.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSourceFactory.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/TeeDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/TeeDataSource.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/TeeDataSource.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/TeeDataSource.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/TransferListener.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/TransferListener.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/TransferListener.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/TransferListener.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java similarity index 98% rename from library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index a2e4382e0c..86dc5cfedf 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java @@ -171,7 +171,7 @@ public final class CacheDataSource implements DataSource { try { uri = dataSpec.uri; flags = dataSpec.flags; - key = dataSpec.key != null ? dataSpec.key : uri.toString(); + key = CacheUtil.getKey(dataSpec); readPosition = dataSpec.position; currentRequestIgnoresCache = (ignoreCacheOnError && seenCacheError) || (dataSpec.length == C.LENGTH_UNSET && ignoreCacheForUnsetLengthRequests); @@ -181,6 +181,9 @@ public final class CacheDataSource implements DataSource { bytesRemaining = cache.getContentLength(key); if (bytesRemaining != C.LENGTH_UNSET) { bytesRemaining -= dataSpec.position; + if (bytesRemaining <= 0) { + throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE); + } } } openNextSource(true); diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java similarity index 97% rename from library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java index fb96c0fb0e..97d55c5fe2 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.upstream.cache; +import android.support.annotation.NonNull; import com.google.android.exoplayer2.C; import java.io.File; @@ -95,7 +96,7 @@ public class CacheSpan implements Comparable { } @Override - public int compareTo(CacheSpan another) { + public int compareTo(@NonNull CacheSpan another) { if (!key.equals(another.key)) { return key.compareTo(another.key); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java new file mode 100644 index 0000000000..f6251dbbf1 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java @@ -0,0 +1,237 @@ +/* + * 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.google.android.exoplayer2.upstream.cache; + +import android.net.Uri; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.PriorityTaskManager; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.util.NavigableSet; + +/** + * Caching related utility methods. + */ +public final class CacheUtil { + + /** Holds the counters used during caching. */ + public static class CachingCounters { + /** Total number of already cached bytes. */ + public long alreadyCachedBytes; + /** + * Total number of downloaded bytes. + * + *

{@link #getCached(DataSpec, Cache, CachingCounters)} sets it to the count of the missing + * bytes or to {@link C#LENGTH_UNSET} if {@code dataSpec} is unbounded and content length isn't + * available in the {@code cache}. + */ + public long downloadedBytes; + } + + /** + * Generates a cache key out of the given {@link Uri}. + * + * @param uri Uri of a content which the requested key is for. + */ + public static String generateKey(Uri uri) { + return uri.toString(); + } + + /** + * Returns the {@code dataSpec.key} if not null, otherwise generates a cache key out of {@code + * dataSpec.uri} + * + * @param dataSpec Defines a content which the requested key is for. + */ + public static String getKey(DataSpec dataSpec) { + return dataSpec.key != null ? dataSpec.key : generateKey(dataSpec.uri); + } + + /** + * Returns already cached and missing bytes in the {@cache} for the data defined by {@code + * dataSpec}. + * + * @param dataSpec Defines the data to be checked. + * @param cache A {@link Cache} which has the data. + * @param counters The counters to be set. If null a new {@link CachingCounters} is created and + * used. + * @return The used {@link CachingCounters} instance. + */ + public static CachingCounters getCached(DataSpec dataSpec, Cache cache, + CachingCounters counters) { + try { + return internalCache(dataSpec, cache, null, null, null, 0, counters); + } catch (IOException | InterruptedException e) { + throw new IllegalStateException(e); + } + } + + /** + * Caches the data defined by {@code dataSpec} while skipping already cached data. + * + * @param dataSpec Defines the data to be cached. + * @param cache A {@link Cache} to store the data. + * @param dataSource A {@link CacheDataSource} that works on the {@code cache}. + * @param buffer The buffer to be used while caching. + * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with + * caching. + * @param priority The priority of this task. Used with {@code priorityTaskManager}. + * @param counters The counters to be set during caching. If not null its values reset to + * zero before using. If null a new {@link CachingCounters} is created and used. + * @return The used {@link CachingCounters} instance. + * @throws IOException If an error occurs reading from the source. + * @throws InterruptedException If the thread was interrupted. + */ + public static CachingCounters cache(DataSpec dataSpec, Cache cache, CacheDataSource dataSource, + byte[] buffer, PriorityTaskManager priorityTaskManager, int priority, + CachingCounters counters) throws IOException, InterruptedException { + Assertions.checkNotNull(dataSource); + Assertions.checkNotNull(buffer); + return internalCache(dataSpec, cache, dataSource, buffer, priorityTaskManager, priority, + counters); + } + + /** + * Caches the data defined by {@code dataSpec} while skipping already cached data. If {@code + * dataSource} or {@code buffer} is null performs a dry run. + * + * @param dataSpec Defines the data to be cached. + * @param cache A {@link Cache} to store the data. + * @param dataSource A {@link CacheDataSource} that works on the {@code cache}. If null a dry run + * is performed. + * @param buffer The buffer to be used while caching. If null a dry run is performed. + * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with + * caching. + * @param priority The priority of this task. Used with {@code priorityTaskManager}. + * @param counters The counters to be set during caching. If not null its values reset to + * zero before using. If null a new {@link CachingCounters} is created and used. + * @return The used {@link CachingCounters} instance. + * @throws IOException If not dry run and an error occurs reading from the source. + * @throws InterruptedException If not dry run and the thread was interrupted. + */ + private static CachingCounters internalCache(DataSpec dataSpec, Cache cache, + CacheDataSource dataSource, byte[] buffer, PriorityTaskManager priorityTaskManager, + int priority, CachingCounters counters) throws IOException, InterruptedException { + long start = dataSpec.position; + long left = dataSpec.length; + String key = getKey(dataSpec); + if (left == C.LENGTH_UNSET) { + left = cache.getContentLength(key); + if (left == C.LENGTH_UNSET) { + left = Long.MAX_VALUE; + } + } + if (counters == null) { + counters = new CachingCounters(); + } else { + counters.alreadyCachedBytes = 0; + counters.downloadedBytes = 0; + } + while (left > 0) { + long blockLength = cache.getCachedBytes(key, start, left); + // Skip already cached data + if (blockLength > 0) { + counters.alreadyCachedBytes += blockLength; + } else { + // There is a hole in the cache which is at least "-blockLength" long. + blockLength = -blockLength; + if (dataSource != null && buffer != null) { + DataSpec subDataSpec = new DataSpec(dataSpec.uri, start, + blockLength == Long.MAX_VALUE ? C.LENGTH_UNSET : blockLength, key); + long read = readAndDiscard(subDataSpec, dataSource, buffer, priorityTaskManager, + priority); + counters.downloadedBytes += read; + if (read < blockLength) { + // Reached end of data. + break; + } + } else if (blockLength == Long.MAX_VALUE) { + counters.downloadedBytes = C.LENGTH_UNSET; + break; + } else { + counters.downloadedBytes += blockLength; + } + } + start += blockLength; + if (left != Long.MAX_VALUE) { + left -= blockLength; + } + } + return counters; + } + + /** + * Reads and discards all data specified by the {@code dataSpec}. + * + * @param dataSpec Defines the data to be read. + * @param dataSource The {@link DataSource} to read the data from. + * @param buffer The buffer to be used while downloading. + * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with + * caching. + * @param priority The priority of this task. + * @return Number of read bytes, or 0 if no data is available because the end of the opened range + * has been reached. + */ + private static long readAndDiscard(DataSpec dataSpec, DataSource dataSource, byte[] buffer, + PriorityTaskManager priorityTaskManager, int priority) + throws IOException, InterruptedException { + while (true) { + if (priorityTaskManager != null) { + // Wait for any other thread with higher priority to finish its job. + priorityTaskManager.proceed(priority); + } + try { + dataSource.open(dataSpec); + long totalRead = 0; + while (true) { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + int read = dataSource.read(buffer, 0, buffer.length); + if (read == C.RESULT_END_OF_INPUT) { + return totalRead; + } + totalRead += read; + } + } catch (PriorityTaskManager.PriorityTooLowException exception) { + // catch and try again + } finally { + Util.closeQuietly(dataSource); + } + } + } + + /** Removes all of the data in the {@code cache} pointed by the {@code key}. */ + public static void remove(Cache cache, String key) { + NavigableSet cachedSpans = cache.getCachedSpans(key); + if (cachedSpans == null) { + return; + } + for (CacheSpan cachedSpan : cachedSpans) { + try { + cache.removeSpan(cachedSpan); + } catch (Cache.CacheException e) { + // do nothing + } + } + } + + private CacheUtil() {} + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java similarity index 98% rename from library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java index 0f08ca40f2..9559054f6d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.upstream.cache; +import android.support.annotation.NonNull; import android.util.Log; import com.google.android.exoplayer2.extractor.ChunkIndex; import java.util.Arrays; @@ -195,7 +196,7 @@ public final class CachedRegionTracker implements Cache.Listener { } @Override - public int compareTo(Region another) { + public int compareTo(@NonNull Region another) { return startOffset < another.startOffset ? -1 : startOffset == another.startOffset ? 0 : 1; } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java rename to library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java diff --git a/library/src/main/java/com/google/android/exoplayer2/util/Assertions.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/util/Assertions.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/Assertions.java diff --git a/library/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java b/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java similarity index 97% rename from library/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java index c383c01453..f2e30d981b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.util; +import android.support.annotation.NonNull; import android.util.Log; import java.io.File; import java.io.FileInputStream; @@ -185,12 +186,12 @@ public final class AtomicFile { } @Override - public void write(byte[] b) throws IOException { + public void write(@NonNull byte[] b) throws IOException { fileOutputStream.write(b); } @Override - public void write(byte[] b, int off, int len) throws IOException { + public void write(@NonNull byte[] b, int off, int len) throws IOException { fileOutputStream.write(b, off, len); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/util/Clock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/util/Clock.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java diff --git a/library/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java similarity index 88% rename from library/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java index f6f3e2a400..0093c3b826 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java @@ -75,6 +75,8 @@ public final class CodecSpecificDataUtil { private static final int AUDIO_OBJECT_TYPE_ER_BSAC = 22; // Parametric Stereo. private static final int AUDIO_OBJECT_TYPE_PS = 29; + // Escape code for extended audio object types. + private static final int AUDIO_OBJECT_TYPE_ESCAPE = 31; private CodecSpecificDataUtil() {} @@ -86,15 +88,8 @@ public final class CodecSpecificDataUtil { */ public static Pair parseAacAudioSpecificConfig(byte[] audioSpecificConfig) { ParsableBitArray bitArray = new ParsableBitArray(audioSpecificConfig); - int audioObjectType = bitArray.readBits(5); - int frequencyIndex = bitArray.readBits(4); - int sampleRate; - if (frequencyIndex == AUDIO_SPECIFIC_CONFIG_FREQUENCY_INDEX_ARBITRARY) { - sampleRate = bitArray.readBits(24); - } else { - Assertions.checkArgument(frequencyIndex < 13); - sampleRate = AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE[frequencyIndex]; - } + int audioObjectType = getAacAudioObjectType(bitArray); + int sampleRate = getAacSamplingFrequency(bitArray); int channelConfiguration = bitArray.readBits(4); if (audioObjectType == AUDIO_OBJECT_TYPE_SBR || audioObjectType == AUDIO_OBJECT_TYPE_PS) { // For an AAC bitstream using spectral band replication (SBR) or parametric stereo (PS) with @@ -102,14 +97,8 @@ public final class CodecSpecificDataUtil { // content; this is identical to the sample rate of the decoded output but may differ from // the sample rate set above. // Use the extensionSamplingFrequencyIndex. - frequencyIndex = bitArray.readBits(4); - if (frequencyIndex == AUDIO_SPECIFIC_CONFIG_FREQUENCY_INDEX_ARBITRARY) { - sampleRate = bitArray.readBits(24); - } else { - Assertions.checkArgument(frequencyIndex < 13); - sampleRate = AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE[frequencyIndex]; - } - audioObjectType = bitArray.readBits(5); + sampleRate = getAacSamplingFrequency(bitArray); + audioObjectType = getAacAudioObjectType(bitArray); if (audioObjectType == AUDIO_OBJECT_TYPE_ER_BSAC) { // Use the extensionChannelConfiguration. channelConfiguration = bitArray.readBits(4); @@ -247,4 +236,37 @@ public final class CodecSpecificDataUtil { return true; } + /** + * Returns the AAC audio object type as specified in 14496-3 (2005) Table 1.14. + * + * @param bitArray The bit array containing the audio specific configuration. + * @return The audio object type. + */ + private static int getAacAudioObjectType(ParsableBitArray bitArray) { + int audioObjectType = bitArray.readBits(5); + if (audioObjectType == AUDIO_OBJECT_TYPE_ESCAPE) { + audioObjectType = 32 + bitArray.readBits(6); + } + return audioObjectType; + } + + /** + * Returns the AAC sampling frequency (or extension sampling frequency) as specified in 14496-3 + * (2005) Table 1.13. + * + * @param bitArray The bit array containing the audio specific configuration. + * @return The sampling frequency. + */ + private static int getAacSamplingFrequency(ParsableBitArray bitArray) { + int samplingFrequency; + int frequencyIndex = bitArray.readBits(4); + if (frequencyIndex == AUDIO_SPECIFIC_CONFIG_FREQUENCY_INDEX_ARBITRARY) { + samplingFrequency = bitArray.readBits(24); + } else { + Assertions.checkArgument(frequencyIndex < 13); + samplingFrequency = AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE[frequencyIndex]; + } + return samplingFrequency; + } + } diff --git a/library/src/main/java/com/google/android/exoplayer2/util/ColorParser.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ColorParser.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/util/ColorParser.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/ColorParser.java diff --git a/library/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java diff --git a/library/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java diff --git a/library/src/main/java/com/google/android/exoplayer2/util/LibraryLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/util/LibraryLoader.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/util/LibraryLoader.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/LibraryLoader.java diff --git a/library/src/main/java/com/google/android/exoplayer2/util/LongArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/LongArray.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/util/LongArray.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/LongArray.java diff --git a/library/src/main/java/com/google/android/exoplayer2/util/PriorityHandlerThread.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MediaClock.java similarity index 51% rename from library/src/main/java/com/google/android/exoplayer2/util/PriorityHandlerThread.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/MediaClock.java index d89bb6bca3..a10298e456 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/PriorityHandlerThread.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MediaClock.java @@ -15,29 +15,30 @@ */ package com.google.android.exoplayer2.util; -import android.os.HandlerThread; -import android.os.Process; +import com.google.android.exoplayer2.PlaybackParameters; /** - * A {@link HandlerThread} with a specified process priority. + * Tracks the progression of media time. */ -public final class PriorityHandlerThread extends HandlerThread { - - private final int priority; +public interface MediaClock { /** - * @param name The name of the thread. - * @param priority The priority level. See {@link Process#setThreadPriority(int)} for details. + * Returns the current media position in microseconds. */ - public PriorityHandlerThread(String name, int priority) { - super(name); - this.priority = priority; - } + long getPositionUs(); - @Override - public void run() { - Process.setThreadPriority(priority); - super.run(); - } + /** + * Attempts to set the playback parameters and returns the active playback parameters, which may + * differ from those passed in. + * + * @param playbackParameters The playback parameters. + * @return The active playback parameters. + */ + PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters); + + /** + * Returns the active playback parameters. + */ + PlaybackParameters getPlaybackParameters(); } diff --git a/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java similarity index 98% rename from library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index 69d4229186..ea669e6f2a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -81,6 +81,7 @@ public final class MimeTypes { public static final String APPLICATION_SCTE35 = BASE_TYPE_APPLICATION + "/x-scte35"; public static final String APPLICATION_CAMERA_MOTION = BASE_TYPE_APPLICATION + "/x-camera-motion"; public static final String APPLICATION_EMSG = BASE_TYPE_APPLICATION + "/x-emsg"; + public static final String APPLICATION_DVBSUBS = BASE_TYPE_APPLICATION + "/dvbsubs"; private MimeTypes() {} @@ -222,7 +223,7 @@ public final class MimeTypes { || APPLICATION_SUBRIP.equals(mimeType) || APPLICATION_TTML.equals(mimeType) || APPLICATION_TX3G.equals(mimeType) || APPLICATION_MP4VTT.equals(mimeType) || APPLICATION_RAWCC.equals(mimeType) || APPLICATION_VOBSUB.equals(mimeType) - || APPLICATION_PGS.equals(mimeType)) { + || APPLICATION_PGS.equals(mimeType) || APPLICATION_DVBSUBS.equals(mimeType)) { return C.TRACK_TYPE_TEXT; } else if (APPLICATION_ID3.equals(mimeType) || APPLICATION_EMSG.equals(mimeType) diff --git a/library/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java diff --git a/library/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java similarity index 74% rename from library/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java index 83f83c7921..df9f04f067 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java @@ -89,6 +89,16 @@ public final class ParsableBitArray { return byteOffset * 8 + bitOffset; } + /** + * Returns the current byte offset. Must only be called when the position is byte aligned. + * + * @throws IllegalStateException If the position isn't byte aligned. + */ + public int getBytePosition() { + Assertions.checkState(bitOffset == 0); + return byteOffset; + } + /** * Sets the current bit offset. * @@ -177,6 +187,47 @@ public final class ParsableBitArray { return returnValue; } + /** + * Aligns the position to the next byte boundary. Does nothing if the position is already aligned. + */ + public void byteAlign() { + if (bitOffset == 0) { + return; + } + bitOffset = 0; + byteOffset++; + assertValidOffset(); + } + + /** + * Reads the next {@code length} bytes into {@code buffer}. Must only be called when the position + * is byte aligned. + * + * @see System#arraycopy(Object, int, Object, int, int) + * @param buffer The array into which the read data should be written. + * @param offset The offset in {@code buffer} at which the read data should be written. + * @param length The number of bytes to read. + * @throws IllegalStateException If the position isn't byte aligned. + */ + public void readBytes(byte[] buffer, int offset, int length) { + Assertions.checkState(bitOffset == 0); + System.arraycopy(data, byteOffset, buffer, offset, length); + byteOffset += length; + assertValidOffset(); + } + + /** + * Skips the next {@code length} bytes. Must only be called when the position is byte aligned. + * + * @param length The number of bytes to read. + * @throws IllegalStateException If the position isn't byte aligned. + */ + public void skipBytes(int length) { + Assertions.checkState(bitOffset == 0); + byteOffset += length; + assertValidOffset(); + } + private void assertValidOffset() { // It is fine for position to be at the end of the array, but no further. Assertions.checkState(byteOffset >= 0 diff --git a/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java similarity index 98% rename from library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java index ef4aa05cfe..2a907e5955 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java @@ -201,6 +201,14 @@ public final class ParsableByteArray { return (data[position] & 0xFF); } + /** + * Peeks at the next char. + */ + public char peekChar() { + return (char) ((data[position] & 0xFF) << 8 + | (data[position + 1] & 0xFF)); + } + /** * Reads the next byte as an unsigned value. */ diff --git a/library/src/main/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java diff --git a/library/src/main/java/com/google/android/exoplayer2/util/Predicate.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Predicate.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/util/Predicate.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/Predicate.java diff --git a/library/src/main/java/com/google/android/exoplayer2/util/PriorityTaskManager.java b/library/core/src/main/java/com/google/android/exoplayer2/util/PriorityTaskManager.java similarity index 97% rename from library/src/main/java/com/google/android/exoplayer2/util/PriorityTaskManager.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/PriorityTaskManager.java index fb61d3ba4a..2516b538c6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/PriorityTaskManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/PriorityTaskManager.java @@ -54,7 +54,7 @@ public final class PriorityTaskManager { /** * Register a new task. The task must call {@link #remove(int)} when done. * - * @param priority The priority of the task. + * @param priority The priority of the task. Larger values indicate higher priorities. */ public void add(int priority) { synchronized (lock) { diff --git a/library/src/main/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStream.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStream.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStream.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStream.java diff --git a/library/src/main/java/com/google/android/exoplayer2/util/SlidingPercentile.java b/library/core/src/main/java/com/google/android/exoplayer2/util/SlidingPercentile.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/util/SlidingPercentile.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/SlidingPercentile.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java new file mode 100644 index 0000000000..5b8d117dd0 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java @@ -0,0 +1,111 @@ +/* + * 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.google.android.exoplayer2.util; + +import android.os.SystemClock; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.PlaybackParameters; + +/** + * A {@link MediaClock} whose position advances with real time based on the playback parameters when + * started. + */ +public final class StandaloneMediaClock implements MediaClock { + + private boolean started; + private long baseUs; + private long baseElapsedMs; + private PlaybackParameters playbackParameters; + + /** + * Creates a new standalone media clock. + */ + public StandaloneMediaClock() { + playbackParameters = PlaybackParameters.DEFAULT; + } + + /** + * Starts the clock. Does nothing if the clock is already started. + */ + public void start() { + if (!started) { + baseElapsedMs = SystemClock.elapsedRealtime(); + started = true; + } + } + + /** + * Stops the clock. Does nothing if the clock is already stopped. + */ + public void stop() { + if (started) { + setPositionUs(getPositionUs()); + started = false; + } + } + + /** + * Sets the clock's position. + * + * @param positionUs The position to set in microseconds. + */ + public void setPositionUs(long positionUs) { + baseUs = positionUs; + if (started) { + baseElapsedMs = SystemClock.elapsedRealtime(); + } + } + + /** + * Synchronizes this clock with the current state of {@code clock}. + * + * @param clock The clock with which to synchronize. + */ + public void synchronize(MediaClock clock) { + setPositionUs(clock.getPositionUs()); + playbackParameters = clock.getPlaybackParameters(); + } + + @Override + public long getPositionUs() { + long positionUs = baseUs; + if (started) { + long elapsedSinceBaseMs = SystemClock.elapsedRealtime() - baseElapsedMs; + if (playbackParameters.speed == 1f) { + positionUs += C.msToUs(elapsedSinceBaseMs); + } else { + positionUs += playbackParameters.getSpeedAdjustedDurationUs(elapsedSinceBaseMs); + } + } + return positionUs; + } + + @Override + public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { + // Store the current position as the new base, in case the playback speed has changed. + if (started) { + setPositionUs(getPositionUs()); + } + this.playbackParameters = playbackParameters; + return playbackParameters; + } + + @Override + public PlaybackParameters getPlaybackParameters() { + return playbackParameters; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/util/SystemClock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/util/SystemClock.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java diff --git a/library/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java b/library/core/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java diff --git a/library/src/main/java/com/google/android/exoplayer2/util/TraceUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/TraceUtil.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/util/TraceUtil.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/TraceUtil.java diff --git a/library/src/main/java/com/google/android/exoplayer2/util/UriUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/UriUtil.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/util/UriUtil.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/UriUtil.java diff --git a/library/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java similarity index 96% rename from library/src/main/java/com/google/android/exoplayer2/util/Util.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index d9282700d7..206349fa07 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -25,6 +25,7 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.graphics.Point; import android.net.Uri; import android.os.Build; +import android.support.annotation.NonNull; import android.text.TextUtils; import android.util.Log; import android.view.Display; @@ -45,6 +46,7 @@ import java.nio.charset.Charset; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; +import java.util.Formatter; import java.util.GregorianCalendar; import java.util.List; import java.util.Locale; @@ -198,7 +200,7 @@ public final class Util { public static ExecutorService newSingleThreadExecutor(final String threadName) { return Executors.newSingleThreadExecutor(new ThreadFactory() { @Override - public Thread newThread(Runnable r) { + public Thread newThread(@NonNull Runnable r) { return new Thread(r, threadName); } }); @@ -309,6 +311,30 @@ public final class Util { return Math.max(min, Math.min(value, max)); } + /** + * Constrains a value to the specified bounds. + * + * @param value The value to constrain. + * @param min The lower bound. + * @param max The upper bound. + * @return The constrained value {@code Math.max(min, Math.min(value, max))}. + */ + public static long constrainValue(long value, long min, long max) { + return Math.max(min, Math.min(value, max)); + } + + /** + * Constrains a value to the specified bounds. + * + * @param value The value to constrain. + * @param min The lower bound. + * @param max The upper bound. + * @return The constrained value {@code Math.max(min, Math.min(value, max))}. + */ + public static float constrainValue(float value, float min, float max) { + return Math.max(min, Math.min(value, max)); + } + /** * Returns the index of the largest element in {@code array} that is less than (or optionally * equal to) a specified {@code value}. @@ -740,7 +766,7 @@ public final class Util { versionName = "?"; } return applicationName + "/" + versionName + " (Linux;Android " + Build.VERSION.RELEASE - + ") " + "ExoPlayerLib/" + ExoPlayerLibraryInfo.VERSION; + + ") " + ExoPlayerLibraryInfo.VERSION_SLASHY; } /** @@ -823,6 +849,27 @@ public final class Util { } } + /** + * Returns the specified millisecond time formatted as a string. + * + * @param builder The builder that {@code formatter} will write to. + * @param formatter The formatter. + * @param timeMs The time to format as a string, in milliseconds. + * @return The time formatted as a string. + */ + public static String getStringForTime(StringBuilder builder, Formatter formatter, long timeMs) { + if (timeMs == C.TIME_UNSET) { + timeMs = 0; + } + long totalSeconds = (timeMs + 500) / 1000; + long seconds = totalSeconds % 60; + long minutes = (totalSeconds / 60) % 60; + long hours = totalSeconds / 3600; + builder.setLength(0); + return hours > 0 ? formatter.format("%d:%02d:%02d", hours, minutes, seconds).toString() + : formatter.format("%02d:%02d", minutes, seconds).toString(); + } + /** * Maps a {@link C} {@code TRACK_TYPE_*} constant to the corresponding {@link C} * {@code DEFAULT_*_BUFFER_SIZE} constant. diff --git a/library/src/main/java/com/google/android/exoplayer2/util/XmlPullParserUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/XmlPullParserUtil.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/util/XmlPullParserUtil.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/XmlPullParserUtil.java diff --git a/library/src/main/java/com/google/android/exoplayer2/video/AvcConfig.java b/library/core/src/main/java/com/google/android/exoplayer2/video/AvcConfig.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/video/AvcConfig.java rename to library/core/src/main/java/com/google/android/exoplayer2/video/AvcConfig.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java new file mode 100644 index 0000000000..7bdc43f85c --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java @@ -0,0 +1,149 @@ +/* + * 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.google.android.exoplayer2.video; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import java.util.Arrays; + +/** + * Stores color info. + */ +public final class ColorInfo implements Parcelable { + + /** + * The color space of the video. Valid values are {@link C#COLOR_SPACE_BT601}, {@link + * C#COLOR_SPACE_BT709}, {@link C#COLOR_SPACE_BT2020} or {@link Format#NO_VALUE} if unknown. + */ + @C.ColorSpace + public final int colorSpace; + + /** + * The color range of the video. Valid values are {@link C#COLOR_RANGE_LIMITED}, {@link + * C#COLOR_RANGE_FULL} or {@link Format#NO_VALUE} if unknown. + */ + @C.ColorRange + public final int colorRange; + + /** + * The color transfer characteristicks of the video. Valid values are {@link + * C#COLOR_TRANSFER_HLG}, {@link C#COLOR_TRANSFER_ST2084}, {@link C#COLOR_TRANSFER_SDR} or {@link + * Format#NO_VALUE} if unknown. + */ + @C.ColorTransfer + public final int colorTransfer; + + /** + * HdrStaticInfo as defined in CTA-861.3. + */ + public final byte[] hdrStaticInfo; + + // Lazily initialized hashcode. + private int hashCode; + + /** + * Constructs the ColorInfo. + * + * @param colorSpace The color space of the video. + * @param colorRange The color range of the video. + * @param colorTransfer The color transfer characteristics of the video. + * @param hdrStaticInfo HdrStaticInfo as defined in CTA-861.3. + */ + public ColorInfo(@C.ColorSpace int colorSpace, @C.ColorRange int colorRange, + @C.ColorTransfer int colorTransfer, byte[] hdrStaticInfo) { + this.colorSpace = colorSpace; + this.colorRange = colorRange; + this.colorTransfer = colorTransfer; + this.hdrStaticInfo = hdrStaticInfo; + } + + @SuppressWarnings("ResourceType") + /* package */ ColorInfo(Parcel in) { + colorSpace = in.readInt(); + colorRange = in.readInt(); + colorTransfer = in.readInt(); + boolean hasHdrStaticInfo = in.readInt() != 0; + hdrStaticInfo = hasHdrStaticInfo ? in.createByteArray() : null; + } + + // Parcelable implementation. + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ColorInfo other = (ColorInfo) obj; + if (colorSpace != other.colorSpace || colorRange != other.colorRange + || colorTransfer != other.colorTransfer + || !Arrays.equals(hdrStaticInfo, other.hdrStaticInfo)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "ColorInfo(" + colorSpace + ", " + colorRange + ", " + colorTransfer + + ", " + (hdrStaticInfo != null) + ")"; + } + + @Override + public int hashCode() { + if (hashCode == 0) { + int result = 17; + result = 31 * result + colorSpace; + result = 31 * result + colorRange; + result = 31 * result + colorTransfer; + result = 31 * result + Arrays.hashCode(hdrStaticInfo); + hashCode = result; + } + return hashCode; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(colorSpace); + dest.writeInt(colorRange); + dest.writeInt(colorTransfer); + dest.writeInt(hdrStaticInfo != null ? 1 : 0); + if (hdrStaticInfo != null) { + dest.writeByteArray(hdrStaticInfo); + } + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override + public ColorInfo createFromParcel(Parcel in) { + return new ColorInfo(in); + } + + @Override + public ColorInfo[] newArray(int size) { + return new ColorInfo[0]; + } + }; + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java b/library/core/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java rename to library/core/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java diff --git a/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java similarity index 93% rename from library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java rename to library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 059628e0c8..ac4bb36035 100644 --- a/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -25,6 +25,7 @@ import android.media.MediaCrypto; import android.media.MediaFormat; import android.os.Handler; import android.os.SystemClock; +import android.support.annotation.NonNull; import android.util.Log; import android.view.Surface; import com.google.android.exoplayer2.C; @@ -85,10 +86,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private int currentHeight; private int currentUnappliedRotationDegrees; private float currentPixelWidthHeightRatio; - private int lastReportedWidth; - private int lastReportedHeight; - private int lastReportedUnappliedRotationDegrees; - private float lastReportedPixelWidthHeightRatio; + private int reportedWidth; + private int reportedHeight; + private int reportedUnappliedRotationDegrees; + private float reportedPixelWidthHeightRatio; private boolean tunneling; private int tunnelingAudioSessionId; @@ -165,7 +166,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { currentPixelWidthHeightRatio = Format.NO_VALUE; pendingPixelWidthHeightRatio = Format.NO_VALUE; scalingMode = C.VIDEO_SCALING_MODE_DEFAULT; - clearLastReportedVideoSize(); + clearReportedVideoSize(); } @Override @@ -228,8 +229,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { super.onPositionReset(positionUs, joining); clearRenderedFirstFrame(); consecutiveDroppedFrameCount = 0; - joiningDeadlineMs = joining && allowedJoiningTimeMs > 0 - ? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET; + if (joining) { + setJoiningDeadlineMs(); + } else { + joiningDeadlineMs = C.TIME_UNSET; + } } @Override @@ -256,11 +260,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { super.onStarted(); droppedFrames = 0; droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime(); + joiningDeadlineMs = C.TIME_UNSET; } @Override protected void onStopped() { - joiningDeadlineMs = C.TIME_UNSET; maybeNotifyDroppedFrames(); super.onStopped(); } @@ -271,7 +275,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { currentHeight = Format.NO_VALUE; currentPixelWidthHeightRatio = Format.NO_VALUE; pendingPixelWidthHeightRatio = Format.NO_VALUE; - clearLastReportedVideoSize(); + clearReportedVideoSize(); + clearRenderedFirstFrame(); frameReleaseTimeHelper.disable(); tunnelingOnFrameRenderedListener = null; try { @@ -311,11 +316,25 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { maybeInitCodec(); } } + if (surface != null) { + // If we know the video size, report it again immediately. + maybeRenotifyVideoSizeChanged(); + // We haven't rendered to the new surface yet. + clearRenderedFirstFrame(); + if (state == STATE_STARTED) { + setJoiningDeadlineMs(); + } + } else { + // The surface has been removed. + clearReportedVideoSize(); + clearRenderedFirstFrame(); + } + } else if (surface != null) { + // The surface is unchanged and non-null. If we know the video size and/or have already + // rendered to the surface, report these again immediately. + maybeRenotifyVideoSizeChanged(); + maybeRenotifyRenderedFirstFrame(); } - // Clear state so that we always call the event listener with the video size and when a frame - // is rendered, even if the surface hasn't changed. - clearRenderedFirstFrame(); - clearLastReportedVideoSize(); } @Override @@ -520,6 +539,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { maybeNotifyRenderedFirstFrame(); } + private void setJoiningDeadlineMs() { + joiningDeadlineMs = allowedJoiningTimeMs > 0 + ? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET; + } + private void clearRenderedFirstFrame() { renderedFirstFrame = false; // The first frame notification is triggered by renderOutputBuffer or renderOutputBufferV21 for @@ -542,23 +566,36 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } } - private void clearLastReportedVideoSize() { - lastReportedWidth = Format.NO_VALUE; - lastReportedHeight = Format.NO_VALUE; - lastReportedPixelWidthHeightRatio = Format.NO_VALUE; - lastReportedUnappliedRotationDegrees = Format.NO_VALUE; + private void maybeRenotifyRenderedFirstFrame() { + if (renderedFirstFrame) { + eventDispatcher.renderedFirstFrame(surface); + } + } + + private void clearReportedVideoSize() { + reportedWidth = Format.NO_VALUE; + reportedHeight = Format.NO_VALUE; + reportedPixelWidthHeightRatio = Format.NO_VALUE; + reportedUnappliedRotationDegrees = Format.NO_VALUE; } private void maybeNotifyVideoSizeChanged() { - if (lastReportedWidth != currentWidth || lastReportedHeight != currentHeight - || lastReportedUnappliedRotationDegrees != currentUnappliedRotationDegrees - || lastReportedPixelWidthHeightRatio != currentPixelWidthHeightRatio) { + if (reportedWidth != currentWidth || reportedHeight != currentHeight + || reportedUnappliedRotationDegrees != currentUnappliedRotationDegrees + || reportedPixelWidthHeightRatio != currentPixelWidthHeightRatio) { + eventDispatcher.videoSizeChanged(currentWidth, currentHeight, currentUnappliedRotationDegrees, + currentPixelWidthHeightRatio); + reportedWidth = currentWidth; + reportedHeight = currentHeight; + reportedUnappliedRotationDegrees = currentUnappliedRotationDegrees; + reportedPixelWidthHeightRatio = currentPixelWidthHeightRatio; + } + } + + private void maybeRenotifyVideoSizeChanged() { + if (reportedWidth != Format.NO_VALUE || reportedHeight != Format.NO_VALUE) { eventDispatcher.videoSizeChanged(currentWidth, currentHeight, currentUnappliedRotationDegrees, currentPixelWidthHeightRatio); - lastReportedWidth = currentWidth; - lastReportedHeight = currentHeight; - lastReportedUnappliedRotationDegrees = currentUnappliedRotationDegrees; - lastReportedPixelWidthHeightRatio = currentPixelWidthHeightRatio; } } @@ -823,7 +860,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } @Override - public void onFrameRendered(MediaCodec codec, long presentationTimeUs, long nanoTime) { + public void onFrameRendered(@NonNull MediaCodec codec, long presentationTimeUs, long nanoTime) { if (this != tunnelingOnFrameRenderedListener) { // Stale event. return; diff --git a/library/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java rename to library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java diff --git a/library/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java rename to library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java diff --git a/library/src/main/javadoc/com/google/android/exoplayer2/doc-files/exoplayer-threading-model.svg b/library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/exoplayer-threading-model.svg similarity index 100% rename from library/src/main/javadoc/com/google/android/exoplayer2/doc-files/exoplayer-threading-model.svg rename to library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/exoplayer-threading-model.svg diff --git a/library/src/main/javadoc/com/google/android/exoplayer2/doc-files/renderer-states.svg b/library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/renderer-states.svg similarity index 100% rename from library/src/main/javadoc/com/google/android/exoplayer2/doc-files/renderer-states.svg rename to library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/renderer-states.svg diff --git a/library/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-advanced.svg b/library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-advanced.svg similarity index 100% rename from library/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-advanced.svg rename to library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-advanced.svg diff --git a/library/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-live-indefinite.svg b/library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-live-indefinite.svg similarity index 100% rename from library/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-live-indefinite.svg rename to library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-live-indefinite.svg diff --git a/library/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-live-limited.svg b/library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-live-limited.svg similarity index 100% rename from library/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-live-limited.svg rename to library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-live-limited.svg diff --git a/library/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-live-multi-period.svg b/library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-live-multi-period.svg similarity index 100% rename from library/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-live-multi-period.svg rename to library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-live-multi-period.svg diff --git a/library/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-period.svg b/library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-period.svg similarity index 100% rename from library/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-period.svg rename to library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-period.svg diff --git a/library/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-playlist.svg b/library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-playlist.svg similarity index 100% rename from library/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-playlist.svg rename to library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-playlist.svg diff --git a/library/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-single-file.svg b/library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-single-file.svg similarity index 100% rename from library/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-single-file.svg rename to library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-single-file.svg diff --git a/library/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-window.svg b/library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-window.svg similarity index 100% rename from library/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-window.svg rename to library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-window.svg diff --git a/library/dash/build.gradle b/library/dash/build.gradle new file mode 100644 index 0000000000..ebad5a8603 --- /dev/null +++ b/library/dash/build.gradle @@ -0,0 +1,56 @@ +// 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. +apply plugin: 'com.android.library' + +android { + compileSdkVersion project.ext.compileSdkVersion + buildToolsVersion project.ext.buildToolsVersion + + defaultConfig { + minSdkVersion project.ext.minSdkVersion + targetSdkVersion project.ext.targetSdkVersion + } + + sourceSets { + androidTest { + java.srcDirs += "../../testutils/src/main/java/" + } + } + + buildTypes { + debug { + testCoverageEnabled = true + } + } +} + +dependencies { + compile project(':library-core') + compile 'com.android.support:support-annotations:' + supportLibraryVersion + compile 'com.android.support:support-core-utils:' + supportLibraryVersion + androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion + androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion + androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion +} + +ext { + javadocTitle = 'DASH module' +} +apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'exoplayer-dash' + releaseDescription = 'The ExoPlayer library DASH module.' +} +apply from: '../../publish.gradle' diff --git a/library/dash/src/androidTest/AndroidManifest.xml b/library/dash/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000000..ac2511d3bd --- /dev/null +++ b/library/dash/src/androidTest/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/library/src/androidTest/assets/dash/sample_mpd_1 b/library/dash/src/androidTest/assets/sample_mpd_1 similarity index 100% rename from library/src/androidTest/assets/dash/sample_mpd_1 rename to library/dash/src/androidTest/assets/sample_mpd_1 diff --git a/library/src/androidTest/assets/dash/sample_mpd_2_unknown_mime_type b/library/dash/src/androidTest/assets/sample_mpd_2_unknown_mime_type similarity index 100% rename from library/src/androidTest/assets/dash/sample_mpd_2_unknown_mime_type rename to library/dash/src/androidTest/assets/sample_mpd_2_unknown_mime_type diff --git a/library/src/androidTest/assets/dash/sample_mpd_3_segment_template b/library/dash/src/androidTest/assets/sample_mpd_3_segment_template similarity index 100% rename from library/src/androidTest/assets/dash/sample_mpd_3_segment_template rename to library/dash/src/androidTest/assets/sample_mpd_3_segment_template diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java new file mode 100644 index 0000000000..c9f1ca1030 --- /dev/null +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java @@ -0,0 +1,82 @@ +/* + * 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.google.android.exoplayer2.source.dash; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; +import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; +import com.google.android.exoplayer2.source.dash.manifest.Period; +import com.google.android.exoplayer2.source.dash.manifest.Representation; +import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase; +import com.google.android.exoplayer2.upstream.DummyDataSource; +import com.google.android.exoplayer2.util.MimeTypes; +import java.util.Arrays; +import junit.framework.TestCase; + +/** + * Unit tests for {@link DashUtil}. + */ +public final class DashUtilTest extends TestCase { + + public void testLoadDrmInitDataFromManifest() throws Exception { + Period period = newPeriod(newAdaptationSets(newRepresentations(newDrmInitData()))); + DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period); + assertEquals(newDrmInitData(), drmInitData); + } + + public void testLoadDrmInitDataMissing() throws Exception { + Period period = newPeriod(newAdaptationSets(newRepresentations(null /* no init data */))); + DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period); + assertNull(drmInitData); + } + + public void testLoadDrmInitDataNoRepresentations() throws Exception { + Period period = newPeriod(newAdaptationSets(/* no representation */)); + DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period); + assertNull(drmInitData); + } + + public void testLoadDrmInitDataNoAdaptationSets() throws Exception { + Period period = newPeriod(/* no adaptation set */); + DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period); + assertNull(drmInitData); + } + + private static Period newPeriod(AdaptationSet... adaptationSets) { + return new Period("", 0, Arrays.asList(adaptationSets)); + } + + private static AdaptationSet newAdaptationSets(Representation... representations) { + return new AdaptationSet(0, C.TRACK_TYPE_VIDEO, Arrays.asList(representations), null); + } + + private static Representation newRepresentations(DrmInitData drmInitData) { + Format format = Format.createVideoContainerFormat("id", MimeTypes.VIDEO_MP4, + MimeTypes.VIDEO_H264, "", Format.NO_VALUE, 1024, 768, Format.NO_VALUE, null, 0); + if (drmInitData != null) { + format = format.copyWithDrmInitData(drmInitData); + } + return Representation.newInstance("", 0, format, "", new SingleSegmentBase()); + } + + private static DrmInitData newDrmInitData() { + return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "mimeType", + new byte[]{1, 4, 7, 0, 3, 6})); + } + +} diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java similarity index 95% rename from library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java rename to library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java index 4de0ae4081..5b8760f929 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java @@ -28,11 +28,9 @@ import java.util.List; */ public class DashManifestParserTest extends InstrumentationTestCase { - private static final String SAMPLE_MPD_1 = "dash/sample_mpd_1"; - private static final String SAMPLE_MPD_2_UNKNOWN_MIME_TYPE = - "dash/sample_mpd_2_unknown_mime_type"; - private static final String SAMPLE_MPD_3_SEGMENT_TEMPLATE = - "dash/sample_mpd_3_segment_template"; + private static final String SAMPLE_MPD_1 = "sample_mpd_1"; + private static final String SAMPLE_MPD_2_UNKNOWN_MIME_TYPE = "sample_mpd_2_unknown_mime_type"; + private static final String SAMPLE_MPD_3_SEGMENT_TEMPLATE = "sample_mpd_3_segment_template"; /** * Simple test to ensure the sample manifests parse without any exceptions being thrown. diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java rename to library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/RangedUriTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/RangedUriTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/RangedUriTest.java rename to library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/RangedUriTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationTest.java rename to library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplateTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplateTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplateTest.java rename to library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplateTest.java diff --git a/library/dash/src/main/AndroidManifest.xml b/library/dash/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..6c2d93510a --- /dev/null +++ b/library/dash/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java rename to library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java rename to library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java similarity index 98% rename from library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java rename to library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index eec99521f1..5ab04ea7be 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -88,7 +88,7 @@ public final class DashMediaSource implements MediaSource { private final int minLoadableRetryCount; private final long livePresentationDelayMs; private final EventDispatcher eventDispatcher; - private final DashManifestParser manifestParser; + private final ParsingLoadable.Parser manifestParser; private final ManifestCallback manifestCallback; private final Object manifestUriLock; private final SparseArray periodsById; @@ -200,15 +200,17 @@ public final class DashMediaSource implements MediaSource { * @param eventListener A listener of events. May be null if delivery of events is not required. */ public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, - DashManifestParser manifestParser, DashChunkSource.Factory chunkSourceFactory, - int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, + ParsingLoadable.Parser manifestParser, + DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, + long livePresentationDelayMs, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { this(null, manifestUri, manifestDataSourceFactory, manifestParser, chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); } private DashMediaSource(DashManifest manifest, Uri manifestUri, - DataSource.Factory manifestDataSourceFactory, DashManifestParser manifestParser, + DataSource.Factory manifestDataSourceFactory, + ParsingLoadable.Parser manifestParser, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { @@ -648,7 +650,7 @@ public final class DashMediaSource implements MediaSource { + Assertions.checkIndex(periodIndex, 0, manifest.getPeriodCount()) : null; return period.set(id, uid, 0, manifest.getPeriodDurationUs(periodIndex), C.msToUs(manifest.getPeriod(periodIndex).startMs - manifest.getPeriod(0).startMs) - - offsetInFirstPeriodUs); + - offsetInFirstPeriodUs, false); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashSegmentIndex.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashSegmentIndex.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/dash/DashSegmentIndex.java rename to library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashSegmentIndex.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java similarity index 65% rename from library/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java rename to library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java index 8fca21b2e0..2febeb8c81 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java @@ -18,14 +18,17 @@ package com.google.android.exoplayer2.source.dash; import android.net.Uri; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.extractor.ChunkIndex; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper; import com.google.android.exoplayer2.source.chunk.InitializationChunk; +import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; +import com.google.android.exoplayer2.source.dash.manifest.Period; import com.google.android.exoplayer2.source.dash.manifest.RangedUri; import com.google.android.exoplayer2.source.dash.manifest.Representation; import com.google.android.exoplayer2.upstream.DataSource; @@ -34,6 +37,7 @@ import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; +import java.util.List; /** * Utility methods for DASH streams. @@ -44,15 +48,14 @@ public final class DashUtil { * Loads a DASH manifest. * * @param dataSource The {@link HttpDataSource} from which the manifest should be read. - * @param manifestUriString The URI of the manifest to be read. + * @param manifestUri The URI of the manifest to be read. * @return An instance of {@link DashManifest}. - * @throws IOException If an error occurs reading data from the stream. - * @see DashManifestParser + * @throws IOException Thrown when there is an error while loading. */ - public static DashManifest loadManifest(DataSource dataSource, String manifestUriString) + public static DashManifest loadManifest(DataSource dataSource, String manifestUri) throws IOException { DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, - new DataSpec(Uri.parse(manifestUriString), DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH)); + new DataSpec(Uri.parse(manifestUri), DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH)); try { inputStream.open(); DashManifestParser parser = new DashManifestParser(); @@ -62,9 +65,78 @@ public final class DashUtil { } } + /** + * Loads {@link DrmInitData} for a given manifest. + * + * @param dataSource The {@link HttpDataSource} from which data should be loaded. + * @param dashManifest The {@link DashManifest} of the DASH content. + * @return The loaded {@link DrmInitData}. + */ + public static DrmInitData loadDrmInitData(DataSource dataSource, DashManifest dashManifest) + throws IOException, InterruptedException { + // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, + // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. + if (dashManifest.getPeriodCount() < 1) { + return null; + } + Period period = dashManifest.getPeriod(0); + int adaptationSetIndex = period.getAdaptationSetIndex(C.TRACK_TYPE_VIDEO); + if (adaptationSetIndex == C.INDEX_UNSET) { + adaptationSetIndex = period.getAdaptationSetIndex(C.TRACK_TYPE_AUDIO); + if (adaptationSetIndex == C.INDEX_UNSET) { + return null; + } + } + AdaptationSet adaptationSet = period.adaptationSets.get(adaptationSetIndex); + if (adaptationSet.representations.isEmpty()) { + return null; + } + Representation representation = adaptationSet.representations.get(0); + DrmInitData drmInitData = representation.format.drmInitData; + if (drmInitData == null) { + Format sampleFormat = DashUtil.loadSampleFormat(dataSource, representation); + if (sampleFormat != null) { + drmInitData = sampleFormat.drmInitData; + } + if (drmInitData == null) { + return null; + } + } + return drmInitData; + } + /** * Loads initialization data for the {@code representation} and returns the sample {@link * Format}. + * Loads {@link DrmInitData} for a given period in a DASH manifest. + * + * @param dataSource The {@link HttpDataSource} from which data should be loaded. + * @param period The {@link Period}. + * @return The loaded {@link DrmInitData}, or null if none is defined. + * @throws IOException Thrown when there is an error while loading. + * @throws InterruptedException Thrown if the thread was interrupted. + */ + public static DrmInitData loadDrmInitData(DataSource dataSource, Period period) + throws IOException, InterruptedException { + Representation representation = getFirstRepresentation(period, C.TRACK_TYPE_VIDEO); + if (representation == null) { + representation = getFirstRepresentation(period, C.TRACK_TYPE_AUDIO); + if (representation == null) { + return null; + } + } + DrmInitData drmInitData = representation.format.drmInitData; + if (drmInitData != null) { + // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, + // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. + return drmInitData; + } + Format sampleFormat = DashUtil.loadSampleFormat(dataSource, representation); + return sampleFormat == null ? null : sampleFormat.drmInitData; + } + + /** + * Loads initialization data for the {@code representation} and returns the sample {@link Format}. * * @param dataSource The source from which the data should be loaded. * @param representation The representation which initialization chunk belongs to. @@ -155,6 +227,15 @@ public final class DashUtil { return new ChunkExtractorWrapper(extractor, format); } + private static Representation getFirstRepresentation(Period period, int type) { + int index = period.getAdaptationSetIndex(type); + if (index == C.INDEX_UNSET) { + return null; + } + List representations = period.adaptationSets.get(index).representations; + return representations.isEmpty() ? null : representations.get(0); + } + private DashUtil() {} } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java similarity index 95% rename from library/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java rename to library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java index 40f3448f6a..8cd5018dc7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java @@ -22,7 +22,7 @@ import com.google.android.exoplayer2.source.dash.manifest.RangedUri; * An implementation of {@link DashSegmentIndex} that wraps a {@link ChunkIndex} parsed from a * media stream. */ -/* package */ final class DashWrappingSegmentIndex implements DashSegmentIndex { +public final class DashWrappingSegmentIndex implements DashSegmentIndex { private final ChunkIndex chunkIndex; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java similarity index 99% rename from library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java rename to library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index a6e909ddac..e679ef635c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -109,8 +109,7 @@ public class DefaultDashChunkSource implements DashChunkSource { * data ranges are adjacent. * @param enableEventMessageTrack Whether the chunks generated by the source may output an event * message track. - * @param enableEventMessageTrack Whether the chunks generated by the source may output a CEA-608 - * track. + * @param enableCea608Track Whether the chunks generated by the source may output a CEA-608 track. */ public DefaultDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, DashManifest manifest, int periodIndex, int adaptationSetIndex, TrackSelection trackSelection, @@ -394,7 +393,7 @@ public class DefaultDashChunkSource implements DashChunkSource { if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) { extractor = new RawCcExtractor(representation.format); } else if (mimeTypeIsWebm(containerMimeType)) { - extractor = new MatroskaExtractor(); + extractor = new MatroskaExtractor(MatroskaExtractor.FLAG_DISABLE_SEEK_FOR_CUES); } else { int flags = 0; if (enableEventMessageTrack) { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java rename to library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java rename to library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java rename to library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java rename to library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java rename to library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java rename to library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationKey.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationKey.java similarity index 96% rename from library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationKey.java rename to library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationKey.java index 51451a83c2..cf17a081d7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationKey.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationKey.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.dash.manifest; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.NonNull; /** * Uniquely identifies a {@link Representation} in a {@link DashManifest}. @@ -68,7 +69,7 @@ public final class RepresentationKey implements Parcelable, Comparable + + + + + + + + + + + + + diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java similarity index 100% rename from library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java rename to library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java similarity index 99% rename from library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java rename to library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java index 3d976353cc..e2eb173df8 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java +++ b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java @@ -70,7 +70,6 @@ public class HlsMediaPlaylistParserTest extends TestCase { try { HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUri, inputStream); assertNotNull(playlist); - assertEquals(HlsPlaylist.TYPE_MEDIA, playlist.type); HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist; assertEquals(HlsMediaPlaylist.PLAYLIST_TYPE_VOD, mediaPlaylist.playlistType); diff --git a/library/hls/src/main/AndroidManifest.xml b/library/hls/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..1ed28f4c7a --- /dev/null +++ b/library/hls/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java rename to library/hls/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsDataSourceFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsDataSourceFactory.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsDataSourceFactory.java rename to library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsDataSourceFactory.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java rename to library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsDataSourceFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsDataSourceFactory.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/hls/HlsDataSourceFactory.java rename to library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsDataSourceFactory.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsManifest.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsManifest.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/hls/HlsManifest.java rename to library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsManifest.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java rename to library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java rename to library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java rename to library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java similarity index 93% rename from library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java rename to library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java index d8eb7e1ae8..450644f60f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java @@ -50,8 +50,8 @@ import java.io.IOException; } @Override - public void skipToKeyframeBefore(long timeUs) { - sampleStreamWrapper.skipToKeyframeBefore(group, timeUs); + public void skipData(long positionUs) { + sampleStreamWrapper.skipData(group, positionUs); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java similarity index 98% rename from library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java rename to library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 8bd966f177..827a6e885d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -312,8 +312,13 @@ import java.util.LinkedList; loadingFinished, lastSeekPositionUs); } - /* package */ void skipToKeyframeBefore(int group, long timeUs) { - sampleQueues.valueAt(group).skipToKeyframeBefore(timeUs); + /* package */ void skipData(int group, long positionUs) { + DefaultTrackOutput sampleQueue = sampleQueues.valueAt(group); + if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { + sampleQueue.skipAll(); + } else { + sampleQueue.skipToKeyframeBefore(positionUs, true); + } } private boolean finishedReadingChunk(HlsMediaChunk chunk) { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java rename to library/hls/src/main/java/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java rename to library/hls/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java similarity index 98% rename from library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java rename to library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java index 5580017805..5a8c63f609 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.source.hls.playlist; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.MimeTypes; - import java.util.Collections; import java.util.List; @@ -56,7 +55,7 @@ public final class HlsMasterPlaylist extends HlsPlaylist { public HlsMasterPlaylist(String baseUri, List variants, List audios, List subtitles, Format muxedAudioFormat, List muxedCaptionFormats) { - super(baseUri, HlsPlaylist.TYPE_MASTER); + super(baseUri); this.variants = Collections.unmodifiableList(variants); this.audios = Collections.unmodifiableList(audios); this.subtitles = Collections.unmodifiableList(subtitles); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java similarity index 98% rename from library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java rename to library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java index 9ef28bdb8d..c7708a1d2f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.hls.playlist; import android.support.annotation.IntDef; +import android.support.annotation.NonNull; import com.google.android.exoplayer2.C; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -61,7 +62,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } @Override - public int compareTo(Long relativeStartTimeUs) { + public int compareTo(@NonNull Long relativeStartTimeUs) { return this.relativeStartTimeUs > relativeStartTimeUs ? 1 : (this.relativeStartTimeUs < relativeStartTimeUs ? -1 : 0); } @@ -96,7 +97,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { long startTimeUs, boolean hasDiscontinuitySequence, int discontinuitySequence, int mediaSequence, int version, long targetDurationUs, boolean hasEndTag, boolean hasProgramDateTime, Segment initializationSegment, List segments) { - super(baseUri, HlsPlaylist.TYPE_MEDIA); + super(baseUri); this.playlistType = playlistType; this.startTimeUs = startTimeUs; this.hasDiscontinuitySequence = hasDiscontinuitySequence; diff --git a/library/src/main/java/com/google/android/exoplayer2/util/MediaClock.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java similarity index 73% rename from library/src/main/java/com/google/android/exoplayer2/util/MediaClock.java rename to library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java index 5f42a40e04..7c3d64d701 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/MediaClock.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java @@ -13,16 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.util; +package com.google.android.exoplayer2.source.hls.playlist; /** - * Tracks the progression of media time. + * Represents an HLS playlist. */ -public interface MediaClock { +public abstract class HlsPlaylist { - /** - * Returns the current media position in microseconds. - */ - long getPositionUs(); + public final String baseUri; + + protected HlsPlaylist(String baseUri) { + this.baseUri = baseUri; + } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java rename to library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java similarity index 97% rename from library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java rename to library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java index 311f279b96..02a8e3f098 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java @@ -477,9 +477,15 @@ public final class HlsPlaylistTracker implements Loader.Callback loadable, long elapsedRealtimeMs, long loadDurationMs) { - processLoadedPlaylist((HlsMediaPlaylist) loadable.getResult()); - eventDispatcher.loadCompleted(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); + HlsPlaylist result = loadable.getResult(); + if (result instanceof HlsMediaPlaylist) { + processLoadedPlaylist((HlsMediaPlaylist) result); + eventDispatcher.loadCompleted(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, + loadDurationMs, loadable.bytesLoaded()); + } else { + onLoadError(loadable, elapsedRealtimeMs, loadDurationMs, + new ParserException("Loaded playlist has unexpected type.")); + } } @Override diff --git a/library/proguard-rules.txt b/library/proguard-rules.txt deleted file mode 100644 index 75f2d095be..0000000000 --- a/library/proguard-rules.txt +++ /dev/null @@ -1,7 +0,0 @@ -# Accessed via reflection in SubtitleDecoderFactory.DEFAULT --keepclassmembers class com.google.android.exoplayer2.text.cea.Cea608Decoder { - public (java.lang.String, int); -} --keepclassmembers class com.google.android.exoplayer2.text.cea.Cea708Decoder { - public (int); -} diff --git a/library/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle new file mode 100644 index 0000000000..81f8234672 --- /dev/null +++ b/library/smoothstreaming/build.gradle @@ -0,0 +1,55 @@ +// 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. +apply plugin: 'com.android.library' + +android { + compileSdkVersion project.ext.compileSdkVersion + buildToolsVersion project.ext.buildToolsVersion + + defaultConfig { + minSdkVersion project.ext.minSdkVersion + targetSdkVersion project.ext.targetSdkVersion + } + + sourceSets { + androidTest { + java.srcDirs += "../../testutils/src/main/java/" + } + } + + buildTypes { + debug { + testCoverageEnabled = true + } + } +} + +dependencies { + compile project(':library-core') + compile 'com.android.support:support-annotations:' + supportLibraryVersion + androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion + androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion + androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion +} + +ext { + javadocTitle = 'SmoothStreaming module' +} +apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'exoplayer-smoothstreaming' + releaseDescription = 'The ExoPlayer library SmoothStreaming module.' +} +apply from: '../../publish.gradle' diff --git a/library/smoothstreaming/src/androidTest/AndroidManifest.xml b/library/smoothstreaming/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000000..9f62e26867 --- /dev/null +++ b/library/smoothstreaming/src/androidTest/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/library/src/androidTest/assets/smoothstreaming/sample_ismc_1 b/library/smoothstreaming/src/androidTest/assets/sample_ismc_1 similarity index 100% rename from library/src/androidTest/assets/smoothstreaming/sample_ismc_1 rename to library/smoothstreaming/src/androidTest/assets/sample_ismc_1 diff --git a/library/src/androidTest/assets/smoothstreaming/sample_ismc_2 b/library/smoothstreaming/src/androidTest/assets/sample_ismc_2 similarity index 100% rename from library/src/androidTest/assets/smoothstreaming/sample_ismc_2 rename to library/smoothstreaming/src/androidTest/assets/sample_ismc_2 diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParserTest.java b/library/smoothstreaming/src/androidTest/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParserTest.java similarity index 90% rename from library/src/androidTest/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParserTest.java rename to library/smoothstreaming/src/androidTest/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParserTest.java index 8116755a0a..4663f014ff 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParserTest.java +++ b/library/smoothstreaming/src/androidTest/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParserTest.java @@ -25,8 +25,8 @@ import java.io.IOException; */ public final class SsManifestParserTest extends InstrumentationTestCase { - private static final String SAMPLE_ISMC_1 = "smoothstreaming/sample_ismc_1"; - private static final String SAMPLE_ISMC_2 = "smoothstreaming/sample_ismc_2"; + private static final String SAMPLE_ISMC_1 = "sample_ismc_1"; + private static final String SAMPLE_ISMC_2 = "sample_ismc_2"; /** * Simple test to ensure the sample manifests parse without any exceptions being thrown. diff --git a/library/smoothstreaming/src/main/AndroidManifest.xml b/library/smoothstreaming/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..49b47f0916 --- /dev/null +++ b/library/smoothstreaming/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java rename to library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java rename to library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java rename to library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java similarity index 97% rename from library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java rename to library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index 0125d45525..e3fb8b606c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -71,7 +71,7 @@ public final class SsMediaSource implements MediaSource, private final int minLoadableRetryCount; private final long livePresentationDelayMs; private final EventDispatcher eventDispatcher; - private final SsManifestParser manifestParser; + private final ParsingLoadable.Parser manifestParser; private final ArrayList mediaPeriods; private Listener sourceListener; @@ -171,15 +171,17 @@ public final class SsMediaSource implements MediaSource, * @param eventListener A listener of events. May be null if delivery of events is not required. */ public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, - SsManifestParser manifestParser, SsChunkSource.Factory chunkSourceFactory, - int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, + ParsingLoadable.Parser manifestParser, + SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, + long livePresentationDelayMs, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { this(null, manifestUri, manifestDataSourceFactory, manifestParser, chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); } private SsMediaSource(SsManifest manifest, Uri manifestUri, - DataSource.Factory manifestDataSourceFactory, SsManifestParser manifestParser, + DataSource.Factory manifestDataSourceFactory, + ParsingLoadable.Parser manifestParser, SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java rename to library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java similarity index 100% rename from library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java rename to library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java deleted file mode 100644 index e271108ce4..0000000000 --- a/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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.google.android.exoplayer2.metadata.id3; - -import android.test.MoreAsserts; -import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.MetadataDecoderException; -import junit.framework.TestCase; - -/** - * Test for {@link Id3Decoder}. - */ -public final class Id3DecoderTest extends TestCase { - - public void testDecodeTxxxFrame() throws MetadataDecoderException { - byte[] rawId3 = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 41, 84, 88, 88, 88, 0, 0, 0, 31, 0, 0, - 3, 0, 109, 100, 105, 97, 108, 111, 103, 95, 86, 73, 78, 68, 73, 67, 79, 49, 53, 50, 55, 54, - 54, 52, 95, 115, 116, 97, 114, 116, 0}; - Id3Decoder decoder = new Id3Decoder(); - Metadata metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(1, metadata.length()); - TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); - assertEquals("TXXX", textInformationFrame.id); - assertEquals("", textInformationFrame.description); - assertEquals("mdialog_VINDICO1527664_start", textInformationFrame.value); - } - - public void testDecodeApicFrame() throws MetadataDecoderException { - byte[] rawId3 = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 45, 65, 80, 73, 67, 0, 0, 0, 35, 0, 0, - 3, 105, 109, 97, 103, 101, 47, 106, 112, 101, 103, 0, 16, 72, 101, 108, 108, 111, 32, 87, - 111, 114, 108, 100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0}; - Id3Decoder decoder = new Id3Decoder(); - Metadata metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(1, metadata.length()); - ApicFrame apicFrame = (ApicFrame) metadata.get(0); - assertEquals("image/jpeg", apicFrame.mimeType); - assertEquals(16, apicFrame.pictureType); - assertEquals("Hello World", apicFrame.description); - assertEquals(10, apicFrame.pictureData.length); - MoreAsserts.assertEquals(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, apicFrame.pictureData); - } - - public void testDecodeTextInformationFrame() throws MetadataDecoderException { - byte[] rawId3 = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 23, 84, 73, 84, 50, 0, 0, 0, 13, 0, 0, - 3, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0}; - Id3Decoder decoder = new Id3Decoder(); - Metadata metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(1, metadata.length()); - TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); - assertEquals("TIT2", textInformationFrame.id); - assertNull(textInformationFrame.description); - assertEquals("Hello World", textInformationFrame.value); - } - - public void testDecodePrivFrame() throws MetadataDecoderException { - byte[] rawId3 = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 19, 80, 82, 73, 86, 0, 0, 0, 9, 0, 0, - 116, 101, 115, 116, 0, 1, 2, 3, 4}; - Id3Decoder decoder = new Id3Decoder(); - Metadata metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(1, metadata.length()); - PrivFrame privFrame = (PrivFrame) metadata.get(0); - assertEquals("test", privFrame.owner); - MoreAsserts.assertEquals(new byte[] {1, 2, 3, 4}, privFrame.privateData); - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java deleted file mode 100644 index 29fad1fbde..0000000000 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * 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.google.android.exoplayer2.extractor; - -import java.util.ArrayList; -import java.util.List; - -/** - * An {@link ExtractorsFactory} that provides an array of extractors for the following formats: - * - *

    - *
  • MP4, including M4A ({@link com.google.android.exoplayer2.extractor.mp4.Mp4Extractor})
  • - *
  • fMP4 ({@link com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor})
  • - *
  • Matroska and WebM ({@link com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor}) - *
  • - *
  • Ogg Vorbis/FLAC ({@link com.google.android.exoplayer2.extractor.ogg.OggExtractor}
  • - *
  • MP3 ({@link com.google.android.exoplayer2.extractor.mp3.Mp3Extractor})
  • - *
  • AAC ({@link com.google.android.exoplayer2.extractor.ts.AdtsExtractor})
  • - *
  • MPEG TS ({@link com.google.android.exoplayer2.extractor.ts.TsExtractor})
  • - *
  • MPEG PS ({@link com.google.android.exoplayer2.extractor.ts.PsExtractor})
  • - *
  • FLV ({@link com.google.android.exoplayer2.extractor.flv.FlvExtractor})
  • - *
  • WAV ({@link com.google.android.exoplayer2.extractor.wav.WavExtractor})
  • - *
  • FLAC (only available if the FLAC extension is built and included)
  • - *
- */ -public final class DefaultExtractorsFactory implements ExtractorsFactory { - - // Lazily initialized default extractor classes in priority order. - private static List> defaultExtractorClasses; - - /** - * Creates a new factory for the default extractors. - */ - public DefaultExtractorsFactory() { - synchronized (DefaultExtractorsFactory.class) { - if (defaultExtractorClasses == null) { - // Lazily initialize defaultExtractorClasses. - List> extractorClasses = new ArrayList<>(); - // We reference extractors using reflection so that they can be deleted cleanly. - // Class.forName is used so that automated tools like proguard can detect the use of - // reflection (see http://proguard.sourceforge.net/FAQ.html#forname). - try { - extractorClasses.add( - Class.forName("com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor") - .asSubclass(Extractor.class)); - } catch (ClassNotFoundException e) { - // Extractor not found. - } - try { - extractorClasses.add( - Class.forName("com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor") - .asSubclass(Extractor.class)); - } catch (ClassNotFoundException e) { - // Extractor not found. - } - try { - extractorClasses.add( - Class.forName("com.google.android.exoplayer2.extractor.mp4.Mp4Extractor") - .asSubclass(Extractor.class)); - } catch (ClassNotFoundException e) { - // Extractor not found. - } - try { - extractorClasses.add( - Class.forName("com.google.android.exoplayer2.extractor.mp3.Mp3Extractor") - .asSubclass(Extractor.class)); - } catch (ClassNotFoundException e) { - // Extractor not found. - } - try { - extractorClasses.add( - Class.forName("com.google.android.exoplayer2.extractor.ts.AdtsExtractor") - .asSubclass(Extractor.class)); - } catch (ClassNotFoundException e) { - // Extractor not found. - } - try { - extractorClasses.add( - Class.forName("com.google.android.exoplayer2.extractor.ts.Ac3Extractor") - .asSubclass(Extractor.class)); - } catch (ClassNotFoundException e) { - // Extractor not found. - } - try { - extractorClasses.add( - Class.forName("com.google.android.exoplayer2.extractor.ts.TsExtractor") - .asSubclass(Extractor.class)); - } catch (ClassNotFoundException e) { - // Extractor not found. - } - try { - extractorClasses.add( - Class.forName("com.google.android.exoplayer2.extractor.flv.FlvExtractor") - .asSubclass(Extractor.class)); - } catch (ClassNotFoundException e) { - // Extractor not found. - } - try { - extractorClasses.add( - Class.forName("com.google.android.exoplayer2.extractor.ogg.OggExtractor") - .asSubclass(Extractor.class)); - } catch (ClassNotFoundException e) { - // Extractor not found. - } - try { - extractorClasses.add( - Class.forName("com.google.android.exoplayer2.extractor.ts.PsExtractor") - .asSubclass(Extractor.class)); - } catch (ClassNotFoundException e) { - // Extractor not found. - } - try { - extractorClasses.add( - Class.forName("com.google.android.exoplayer2.extractor.wav.WavExtractor") - .asSubclass(Extractor.class)); - } catch (ClassNotFoundException e) { - // Extractor not found. - } - try { - extractorClasses.add( - Class.forName("com.google.android.exoplayer2.ext.flac.FlacExtractor") - .asSubclass(Extractor.class)); - } catch (ClassNotFoundException e) { - // Extractor not found. - } - defaultExtractorClasses = extractorClasses; - } - } - } - - @Override - public Extractor[] createExtractors() { - Extractor[] extractors = new Extractor[defaultExtractorClasses.size()]; - for (int i = 0; i < extractors.length; i++) { - try { - extractors[i] = defaultExtractorClasses.get(i).getConstructor().newInstance(); - } catch (Exception e) { - // Should never happen. - throw new IllegalStateException("Unexpected error creating default extractor", e); - } - } - return extractors; - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java deleted file mode 100644 index aecd2fb324..0000000000 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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.google.android.exoplayer2.source.hls.playlist; - -import android.support.annotation.IntDef; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** - * Represents an HLS playlist. - */ -public abstract class HlsPlaylist { - - /** - * The type of playlist. - */ - @Retention(RetentionPolicy.SOURCE) - @IntDef({TYPE_MASTER, TYPE_MEDIA}) - public @interface Type {} - public static final int TYPE_MASTER = 0; - public static final int TYPE_MEDIA = 1; - - public final String baseUri; - @Type public final int type; - - protected HlsPlaylist(String baseUri, @Type int type) { - this.baseUri = baseUri; - this.type = type; - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java b/library/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java deleted file mode 100644 index 7484a5244d..0000000000 --- a/library/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.google.android.exoplayer2.text.tx3g; - -import com.google.android.exoplayer2.text.Cue; -import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; -import com.google.android.exoplayer2.text.Subtitle; -import com.google.android.exoplayer2.util.ParsableByteArray; - -/** - * A {@link SimpleSubtitleDecoder} for tx3g. - *

- * Currently only supports parsing of a single text track. - */ -public final class Tx3gDecoder extends SimpleSubtitleDecoder { - - private final ParsableByteArray parsableByteArray; - - public Tx3gDecoder() { - super("Tx3gDecoder"); - parsableByteArray = new ParsableByteArray(); - } - - @Override - protected Subtitle decode(byte[] bytes, int length) { - parsableByteArray.reset(bytes, length); - int textLength = parsableByteArray.readUnsignedShort(); - if (textLength == 0) { - return Tx3gSubtitle.EMPTY; - } - String cueText = parsableByteArray.readString(textLength); - return new Tx3gSubtitle(new Cue(cueText)); - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java b/library/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java deleted file mode 100644 index 0ae9b40869..0000000000 --- a/library/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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.google.android.exoplayer2.util; - -import android.os.SystemClock; - -/** - * A standalone {@link MediaClock}. The clock can be started, stopped and its time can be set and - * retrieved. When started, this clock is based on {@link SystemClock#elapsedRealtime()}. - */ -public final class StandaloneMediaClock implements MediaClock { - - private boolean started; - - /** - * The media time when the clock was last set or stopped. - */ - private long positionUs; - - /** - * The difference between {@link SystemClock#elapsedRealtime()} and {@link #positionUs} - * when the clock was last set or started. - */ - private long deltaUs; - - /** - * Starts the clock. Does nothing if the clock is already started. - */ - public void start() { - if (!started) { - started = true; - deltaUs = elapsedRealtimeMinus(positionUs); - } - } - - /** - * Stops the clock. Does nothing if the clock is already stopped. - */ - public void stop() { - if (started) { - positionUs = elapsedRealtimeMinus(deltaUs); - started = false; - } - } - - /** - * @param timeUs The position to set in microseconds. - */ - public void setPositionUs(long timeUs) { - this.positionUs = timeUs; - deltaUs = elapsedRealtimeMinus(timeUs); - } - - @Override - public long getPositionUs() { - return started ? elapsedRealtimeMinus(deltaUs) : positionUs; - } - - private long elapsedRealtimeMinus(long toSubtractUs) { - return SystemClock.elapsedRealtime() * 1000 - toSubtractUs; - } - -} diff --git a/library/ui/build.gradle b/library/ui/build.gradle new file mode 100644 index 0000000000..96dcd52655 --- /dev/null +++ b/library/ui/build.gradle @@ -0,0 +1,46 @@ +// 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. +apply plugin: 'com.android.library' + +android { + compileSdkVersion project.ext.compileSdkVersion + buildToolsVersion project.ext.buildToolsVersion + + defaultConfig { + minSdkVersion project.ext.minSdkVersion + targetSdkVersion project.ext.targetSdkVersion + } + + buildTypes { + debug { + testCoverageEnabled = true + } + } +} + +dependencies { + compile project(':library-core') + compile 'com.android.support:support-annotations:' + supportLibraryVersion +} + +ext { + javadocTitle = 'UI module' +} +apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'exoplayer-ui' + releaseDescription = 'The ExoPlayer library UI module.' +} +apply from: '../../publish.gradle' diff --git a/library/ui/src/main/AndroidManifest.xml b/library/ui/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..72189e22a7 --- /dev/null +++ b/library/ui/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java similarity index 99% rename from library/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java rename to library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java index d3034a8bc8..b0df16b484 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java @@ -20,8 +20,6 @@ import android.content.res.TypedArray; import android.support.annotation.IntDef; import android.util.AttributeSet; import android.widget.FrameLayout; - -import com.google.android.exoplayer2.R; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java similarity index 96% rename from library/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java rename to library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java index 1bf5b59a4a..38c7a5be9c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java @@ -19,6 +19,7 @@ import android.widget.TextView; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.decoder.DecoderCounters; @@ -90,6 +91,11 @@ public final class DebugTextViewHelper implements Runnable, ExoPlayer.EventListe updateAndPost(); } + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + // Do nothing. + } + @Override public void onTimelineChanged(Timeline timeline, Object manifest) { // Do nothing. diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java new file mode 100644 index 0000000000..d40da451a2 --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java @@ -0,0 +1,580 @@ +/* + * 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.google.android.exoplayer2.ui; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.util.Formatter; +import java.util.Locale; + +/** + * A time bar that shows a current position, buffered position, duration and ad markers. + */ +public class DefaultTimeBar extends View implements TimeBar { + + /** + * The threshold in dps above the bar at which touch events trigger fine scrub mode. + */ + private static final int FINE_SCRUB_Y_THRESHOLD = -50; + /** + * The ratio by which times are reduced in fine scrub mode. + */ + private static final int FINE_SCRUB_RATIO = 3; + /** + * The time after which the scrubbing listener is notified that scrubbing has stopped after + * performing an incremental scrub using key input. + */ + private static final long STOP_SCRUBBING_TIMEOUT_MS = 1000; + private static final int DEFAULT_INCREMENT_COUNT = 20; + private static final int DEFAULT_BAR_HEIGHT = 4; + private static final int DEFAULT_TOUCH_TARGET_HEIGHT = 26; + private static final int DEFAULT_PLAYED_COLOR = 0x33FFFFFF; + private static final int DEFAULT_BUFFERED_COLOR = 0xCCFFFFFF; + private static final int DEFAULT_AD_MARKER_COLOR = 0xB2FFFF00; + private static final int DEFAULT_AD_MARKER_WIDTH = 4; + private static final int DEFAULT_SCRUBBER_ENABLED_SIZE = 12; + private static final int DEFAULT_SCRUBBER_DISABLED_SIZE = 0; + private static final int DEFAULT_SCRUBBER_DRAGGED_SIZE = 16; + private static final int OPAQUE_COLOR = 0xFF000000; + + private final Rect seekBounds; + private final Rect progressBar; + private final Rect bufferedBar; + private final Rect scrubberBar; + private final Paint progressPaint; + private final Paint bufferedPaint; + private final Paint scrubberPaint; + private final Paint adMarkerPaint; + private final int barHeight; + private final int touchTargetHeight; + private final int adMarkerWidth; + private final int scrubberEnabledSize; + private final int scrubberDisabledSize; + private final int scrubberDraggedSize; + private final int scrubberPadding; + private final int fineScrubYThreshold; + private final StringBuilder formatBuilder; + private final Formatter formatter; + private final Runnable stopScrubbingRunnable; + + private int scrubberSize; + private OnScrubListener listener; + private int keyCountIncrement; + private long keyTimeIncrement; + private int lastCoarseScrubXPosition; + private int[] locationOnScreen; + private Point touchPosition; + + private boolean scrubbing; + private long scrubPosition; + private long duration; + private long position; + private long bufferedPosition; + private int adBreakCount; + private long[] adBreakTimesMs; + + /** + * Creates a new time bar. + */ + public DefaultTimeBar(Context context, AttributeSet attrs) { + super(context, attrs); + seekBounds = new Rect(); + progressBar = new Rect(); + bufferedBar = new Rect(); + scrubberBar = new Rect(); + progressPaint = new Paint(); + bufferedPaint = new Paint(); + scrubberPaint = new Paint(); + adMarkerPaint = new Paint(); + + // Calculate the dimensions and paints for drawn elements. + Resources res = context.getResources(); + DisplayMetrics displayMetrics = res.getDisplayMetrics(); + fineScrubYThreshold = dpToPx(displayMetrics, FINE_SCRUB_Y_THRESHOLD); + int defaultBarHeight = dpToPx(displayMetrics, DEFAULT_BAR_HEIGHT); + int defaultTouchTargetHeight = dpToPx(displayMetrics, DEFAULT_TOUCH_TARGET_HEIGHT); + int defaultAdMarkerWidth = dpToPx(displayMetrics, DEFAULT_AD_MARKER_WIDTH); + int defaultScrubberEnabledSize = dpToPx(displayMetrics, DEFAULT_SCRUBBER_ENABLED_SIZE); + int defaultScrubberDisabledSize = dpToPx(displayMetrics, DEFAULT_SCRUBBER_DISABLED_SIZE); + int defaultScrubberDraggedSize = dpToPx(displayMetrics, DEFAULT_SCRUBBER_DRAGGED_SIZE); + if (attrs != null) { + TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DefaultTimeBar, 0, + 0); + try { + barHeight = a.getDimensionPixelSize(R.styleable.DefaultTimeBar_bar_height, + defaultBarHeight); + touchTargetHeight = a.getDimensionPixelSize(R.styleable.DefaultTimeBar_touch_target_height, + defaultTouchTargetHeight); + adMarkerWidth = a.getDimensionPixelSize(R.styleable.DefaultTimeBar_ad_marker_width, + defaultAdMarkerWidth); + scrubberEnabledSize = a.getDimensionPixelSize( + R.styleable.DefaultTimeBar_scrubber_enabled_size, defaultScrubberEnabledSize); + scrubberDisabledSize = a.getDimensionPixelSize( + R.styleable.DefaultTimeBar_scrubber_disabled_size, defaultScrubberDisabledSize); + scrubberDraggedSize = a.getDimensionPixelSize( + R.styleable.DefaultTimeBar_scrubber_dragged_size, defaultScrubberDraggedSize); + int playedColor = a.getInt(R.styleable.DefaultTimeBar_played_color, DEFAULT_PLAYED_COLOR); + int bufferedColor = a.getInt(R.styleable.DefaultTimeBar_buffered_color, + DEFAULT_BUFFERED_COLOR); + int adMarkerColor = a.getInt(R.styleable.DefaultTimeBar_ad_marker_color, + DEFAULT_AD_MARKER_COLOR); + progressPaint.setColor(playedColor); + scrubberPaint.setColor(OPAQUE_COLOR | playedColor); + bufferedPaint.setColor(bufferedColor); + adMarkerPaint.setColor(adMarkerColor); + } finally { + a.recycle(); + } + } else { + barHeight = defaultBarHeight; + touchTargetHeight = defaultTouchTargetHeight; + adMarkerWidth = defaultAdMarkerWidth; + scrubberEnabledSize = defaultScrubberEnabledSize; + scrubberDisabledSize = defaultScrubberDisabledSize; + scrubberDraggedSize = defaultScrubberDraggedSize; + scrubberPaint.setColor(OPAQUE_COLOR | DEFAULT_PLAYED_COLOR); + progressPaint.setColor(DEFAULT_PLAYED_COLOR); + bufferedPaint.setColor(DEFAULT_BUFFERED_COLOR); + adMarkerPaint.setColor(DEFAULT_AD_MARKER_COLOR); + } + formatBuilder = new StringBuilder(); + formatter = new Formatter(formatBuilder, Locale.getDefault()); + stopScrubbingRunnable = new Runnable() { + @Override + public void run() { + stopScrubbing(false); + } + }; + scrubberSize = scrubberEnabledSize; + scrubberPadding = + (Math.max(scrubberDisabledSize, Math.max(scrubberEnabledSize, scrubberDraggedSize)) + 1) + / 2; + duration = C.TIME_UNSET; + keyTimeIncrement = C.TIME_UNSET; + keyCountIncrement = DEFAULT_INCREMENT_COUNT; + setFocusable(true); + if (Util.SDK_INT >= 16) { + maybeSetImportantForAccessibilityV16(); + } + } + + @Override + public void setListener(OnScrubListener listener) { + this.listener = listener; + } + + @Override + public void setKeyTimeIncrement(long time) { + Assertions.checkArgument(time > 0); + keyCountIncrement = C.INDEX_UNSET; + keyTimeIncrement = time; + } + + @Override + public void setKeyCountIncrement(int count) { + Assertions.checkArgument(count > 0); + keyCountIncrement = count; + keyTimeIncrement = C.TIME_UNSET; + } + + @Override + public void setPosition(long position) { + this.position = position; + setContentDescription(getProgressText()); + } + + @Override + public void setBufferedPosition(long bufferedPosition) { + this.bufferedPosition = bufferedPosition; + } + + @Override + public void setDuration(long duration) { + this.duration = duration; + if (scrubbing && duration == C.TIME_UNSET) { + stopScrubbing(true); + } else { + updateScrubberState(); + } + } + + @Override + public void setAdBreakTimesMs(@Nullable long[] adBreakTimesMs, int adBreakCount) { + Assertions.checkArgument(adBreakCount == 0 || adBreakTimesMs != null); + this.adBreakCount = adBreakCount; + this.adBreakTimesMs = adBreakTimesMs; + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + updateScrubberState(); + if (scrubbing && !enabled) { + stopScrubbing(true); + } + } + + @Override + public void onDraw(Canvas canvas) { + canvas.save(); + drawTimeBar(canvas); + drawPlayhead(canvas); + canvas.restore(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!isEnabled() || duration <= 0) { + return false; + } + Point touchPosition = resolveRelativeTouchPosition(event); + int x = touchPosition.x; + int y = touchPosition.y; + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + if (isInSeekBar(x, y)) { + startScrubbing(); + positionScrubber(x); + scrubPosition = getScrubberPosition(); + update(); + invalidate(); + return true; + } + break; + case MotionEvent.ACTION_MOVE: + if (scrubbing) { + if (y < fineScrubYThreshold) { + int relativeX = x - lastCoarseScrubXPosition; + positionScrubber(lastCoarseScrubXPosition + relativeX / FINE_SCRUB_RATIO); + } else { + lastCoarseScrubXPosition = x; + positionScrubber(x); + } + scrubPosition = getScrubberPosition(); + if (listener != null) { + listener.onScrubMove(this, scrubPosition); + } + update(); + invalidate(); + return true; + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + if (scrubbing) { + stopScrubbing(event.getAction() == MotionEvent.ACTION_CANCEL); + return true; + } + break; + default: + // Do nothing. + } + return false; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (isEnabled()) { + long positionIncrement = getPositionIncrement(); + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + positionIncrement = -positionIncrement; + // Fall through. + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (scrubIncrementally(positionIncrement)) { + removeCallbacks(stopScrubbingRunnable); + postDelayed(stopScrubbingRunnable, STOP_SCRUBBING_TIMEOUT_MS); + return true; + } + break; + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_ENTER: + if (scrubbing) { + removeCallbacks(stopScrubbingRunnable); + stopScrubbingRunnable.run(); + return true; + } + break; + default: + // Do nothing. + } + } + return super.onKeyDown(keyCode, event); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int measureWidth = MeasureSpec.getSize(widthMeasureSpec); + int measureHeight = MeasureSpec.getSize(heightMeasureSpec); + setMeasuredDimension(measureWidth, measureHeight); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int width = right - left; + int height = bottom - top; + int barY = height - touchTargetHeight; + int seekLeft = getPaddingLeft(); + int seekRight = width - getPaddingRight(); + int progressY = barY + (touchTargetHeight - barHeight) / 2; + seekBounds.set(seekLeft, barY, seekRight, barY + touchTargetHeight); + progressBar.set(seekBounds.left + scrubberPadding, progressY, + seekBounds.right - scrubberPadding, progressY + barHeight); + update(); + } + + @Override + protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { + super.onSizeChanged(width, height, oldWidth, oldHeight); + } + + @TargetApi(14) + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SELECTED) { + event.getText().add(getProgressText()); + } + event.setClassName(DefaultTimeBar.class.getName()); + } + + @TargetApi(21) + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(DefaultTimeBar.class.getCanonicalName()); + info.setContentDescription(getProgressText()); + if (duration <= 0) { + return; + } + if (Util.SDK_INT >= 21) { + info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD); + info.addAction(AccessibilityAction.ACTION_SCROLL_BACKWARD); + } else if (Util.SDK_INT >= 16) { + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); + } + } + + @TargetApi(16) + @Override + public boolean performAccessibilityAction(int action, Bundle args) { + if (super.performAccessibilityAction(action, args)) { + return true; + } + if (duration <= 0) { + return false; + } + if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) { + if (scrubIncrementally(-getPositionIncrement())) { + stopScrubbing(false); + } + } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) { + if (scrubIncrementally(getPositionIncrement())) { + stopScrubbing(false); + } + } else { + return false; + } + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + return true; + } + + // Internal methods. + + @TargetApi(16) + private void maybeSetImportantForAccessibilityV16() { + if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + } + + private void startScrubbing() { + scrubbing = true; + updateScrubberState(); + ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + if (listener != null) { + listener.onScrubStart(this); + } + } + + private void stopScrubbing(boolean canceled) { + scrubbing = false; + ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(false); + } + updateScrubberState(); + invalidate(); + if (listener != null) { + listener.onScrubStop(this, getScrubberPosition(), canceled); + } + } + + private void updateScrubberState() { + scrubberSize = scrubbing ? scrubberDraggedSize + : (isEnabled() && duration >= 0 ? scrubberEnabledSize : scrubberDisabledSize); + } + + private void update() { + bufferedBar.set(progressBar); + scrubberBar.set(progressBar); + long newScrubberTime = scrubbing ? scrubPosition : position; + if (duration > 0) { + int bufferedPixelWidth = + (int) ((progressBar.width() * bufferedPosition) / duration); + bufferedBar.right = progressBar.left + bufferedPixelWidth; + int scrubberPixelPosition = + (int) ((progressBar.width() * newScrubberTime) / duration); + scrubberBar.right = progressBar.left + scrubberPixelPosition; + } else { + bufferedBar.right = progressBar.left; + scrubberBar.right = progressBar.left; + } + invalidate(seekBounds); + } + + private void positionScrubber(float xPosition) { + scrubberBar.right = Util.constrainValue((int) xPosition, progressBar.left, progressBar.right); + } + + private Point resolveRelativeTouchPosition(MotionEvent motionEvent) { + if (locationOnScreen == null) { + locationOnScreen = new int[2]; + touchPosition = new Point(); + } + getLocationOnScreen(locationOnScreen); + touchPosition.set( + ((int) motionEvent.getRawX()) - locationOnScreen[0], + ((int) motionEvent.getRawY()) - locationOnScreen[1]); + return touchPosition; + } + + private long getScrubberPosition() { + if (progressBar.width() <= 0 || duration == C.TIME_UNSET) { + return 0; + } + return (scrubberBar.width() * duration) / progressBar.width(); + } + + private boolean isInSeekBar(float x, float y) { + return seekBounds.contains((int) x, (int) y); + } + + private void drawTimeBar(Canvas canvas) { + int progressBarHeight = progressBar.height(); + int barTop = progressBar.centerY() - progressBarHeight / 2; + int barBottom = barTop + progressBarHeight; + if (duration <= 0) { + canvas.drawRect(progressBar.left, barTop, progressBar.right, barBottom, progressPaint); + return; + } + int bufferedLeft = bufferedBar.left; + int bufferedRight = bufferedBar.right; + int progressLeft = Math.max(Math.max(progressBar.left, bufferedRight), scrubberBar.right); + if (progressLeft < progressBar.right) { + canvas.drawRect(progressLeft, barTop, progressBar.right, barBottom, progressPaint); + } + bufferedLeft = Math.max(bufferedLeft, scrubberBar.right); + if (bufferedRight > bufferedLeft) { + canvas.drawRect(bufferedLeft, barTop, bufferedRight, barBottom, bufferedPaint); + } + if (scrubberBar.width() > 0) { + canvas.drawRect(scrubberBar.left, barTop, scrubberBar.right, barBottom, scrubberPaint); + } + int adMarkerOffset = adMarkerWidth / 2; + for (int i = 0; i < adBreakCount; i++) { + long adBreakTimeMs = Util.constrainValue(adBreakTimesMs[i], 0, duration); + int markerPositionOffset = + (int) (progressBar.width() * adBreakTimeMs / duration) - adMarkerOffset; + int markerLeft = progressBar.left + Math.min(progressBar.width() - adMarkerWidth, + Math.max(0, markerPositionOffset)); + canvas.drawRect(markerLeft, barTop, markerLeft + adMarkerWidth, barBottom, adMarkerPaint); + } + } + + private void drawPlayhead(Canvas canvas) { + if (duration <= 0) { + return; + } + int playheadRadius = scrubberSize / 2; + int playheadCenter = Util.constrainValue(scrubberBar.right, scrubberBar.left, + progressBar.right); + canvas.drawCircle(playheadCenter, scrubberBar.centerY(), playheadRadius, scrubberPaint); + } + + private String getProgressText() { + return Util.getStringForTime(formatBuilder, formatter, position); + } + + private long getPositionIncrement() { + return keyTimeIncrement == C.TIME_UNSET + ? (duration == C.TIME_UNSET ? 0 : (duration / keyCountIncrement)) : keyTimeIncrement; + } + + /** + * Incrementally scrubs the position by {@code positionChange}. + * + * @param positionChange The change in the scrubber position, in milliseconds. May be negative. + * @return Returns whether the scrubber position changed. + */ + private boolean scrubIncrementally(long positionChange) { + if (duration <= 0) { + return false; + } + long scrubberPosition = getScrubberPosition(); + scrubPosition = Util.constrainValue(scrubberPosition + positionChange, 0, duration); + if (scrubPosition == scrubberPosition) { + return false; + } + if (!scrubbing) { + startScrubbing(); + } + if (listener != null) { + listener.onScrubMove(this, scrubPosition); + } + update(); + return true; + } + + private static int dpToPx(DisplayMetrics displayMetrics, int dps) { + return (int) (dps * displayMetrics.density + 0.5f); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java similarity index 64% rename from library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java rename to library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index dc3c398357..ce2e81020f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ui; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; @@ -24,16 +25,17 @@ import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.widget.FrameLayout; -import android.widget.SeekBar; import android.widget.TextView; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.R; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; +import java.util.Arrays; import java.util.Formatter; import java.util.Locale; @@ -125,9 +127,9 @@ import java.util.Locale; *

  • Type: {@link TextView}
  • * * - *
  • {@code exo_progress} - Seek bar that's updated during playback and allows seeking. + *
  • {@code exo_progress} - Time bar that's updated during playback and allows seeking. *
      - *
    • Type: {@link SeekBar}
    • + *
    • Type: {@link TimeBar}
    • *
    *
  • * @@ -159,28 +161,50 @@ public class PlaybackControlView extends FrameLayout { } /** - * Dispatches seek operations to the player. + * Dispatches operations to the player. + *

    + * Implementations may choose to suppress (e.g. prevent playback from resuming if audio focus is + * denied) or modify (e.g. change the seek position to prevent a user from seeking past a + * non-skippable advert) operations. */ - public interface SeekDispatcher { + public interface ControlDispatcher { /** - * @param player The player to seek. + * Dispatches a {@link ExoPlayer#setPlayWhenReady(boolean)} operation. + * + * @param player The player to which the operation should be dispatched. + * @param playWhenReady Whether playback should proceed when ready. + * @return True if the operation was dispatched. False if suppressed. + */ + boolean dispatchSetPlayWhenReady(ExoPlayer player, boolean playWhenReady); + + /** + * Dispatches a {@link ExoPlayer#seekTo(int, long)} operation. + * + * @param player The player to which the operation should be dispatched. * @param windowIndex The index of the window. * @param positionMs The seek position in the specified window, or {@link C#TIME_UNSET} to seek * to the window's default position. - * @return True if the seek was dispatched. False otherwise. + * @return True if the operation was dispatched. False if suppressed. */ - boolean dispatchSeek(ExoPlayer player, int windowIndex, long positionMs); + boolean dispatchSeekTo(ExoPlayer player, int windowIndex, long positionMs); } /** - * Default {@link SeekDispatcher} that dispatches seeks to the player without modification. + * Default {@link ControlDispatcher} that dispatches operations to the player without + * modification. */ - public static final SeekDispatcher DEFAULT_SEEK_DISPATCHER = new SeekDispatcher() { + public static final ControlDispatcher DEFAULT_CONTROL_DISPATCHER = new ControlDispatcher() { @Override - public boolean dispatchSeek(ExoPlayer player, int windowIndex, long positionMs) { + public boolean dispatchSetPlayWhenReady(ExoPlayer player, boolean playWhenReady) { + player.setPlayWhenReady(playWhenReady); + return true; + } + + @Override + public boolean dispatchSeekTo(ExoPlayer player, int windowIndex, long positionMs) { player.seekTo(windowIndex, positionMs); return true; } @@ -191,7 +215,11 @@ public class PlaybackControlView extends FrameLayout { public static final int DEFAULT_REWIND_MS = 5000; public static final int DEFAULT_SHOW_TIMEOUT_MS = 5000; - private static final int PROGRESS_BAR_MAX = 1000; + /** + * The maximum number of windows that can be shown in a multi-window time bar. + */ + public static final int MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR = 100; + private static final long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000; private final ComponentListener componentListener; @@ -203,21 +231,25 @@ public class PlaybackControlView extends FrameLayout { private final View rewindButton; private final TextView durationView; private final TextView positionView; - private final SeekBar progressBar; + private final TimeBar timeBar; private final StringBuilder formatBuilder; private final Formatter formatter; - private final Timeline.Window currentWindow; + private final Timeline.Period period; + private final Timeline.Window window; private ExoPlayer player; - private SeekDispatcher seekDispatcher; + private ControlDispatcher controlDispatcher; private VisibilityListener visibilityListener; private boolean isAttachedToWindow; - private boolean dragging; + private boolean showMultiWindowTimeBar; + private boolean multiWindowTimeBar; + private boolean scrubbing; private int rewindMs; private int fastForwardMs; private int showTimeoutMs; private long hideAtMs; + private long[] adBreakTimesMs; private final Runnable updateProgressAction = new Runnable() { @Override @@ -262,21 +294,22 @@ public class PlaybackControlView extends FrameLayout { a.recycle(); } } - currentWindow = new Timeline.Window(); + period = new Timeline.Period(); + window = new Timeline.Window(); formatBuilder = new StringBuilder(); formatter = new Formatter(formatBuilder, Locale.getDefault()); + adBreakTimesMs = new long[0]; componentListener = new ComponentListener(); - seekDispatcher = DEFAULT_SEEK_DISPATCHER; + controlDispatcher = DEFAULT_CONTROL_DISPATCHER; LayoutInflater.from(context).inflate(controllerLayoutId, this); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); durationView = (TextView) findViewById(R.id.exo_duration); positionView = (TextView) findViewById(R.id.exo_position); - progressBar = (SeekBar) findViewById(R.id.exo_progress); - if (progressBar != null) { - progressBar.setOnSeekBarChangeListener(componentListener); - progressBar.setMax(PROGRESS_BAR_MAX); + timeBar = (TimeBar) findViewById(R.id.exo_progress); + if (timeBar != null) { + timeBar.setListener(componentListener); } playButton = findViewById(R.id.exo_play); if (playButton != null) { @@ -314,7 +347,7 @@ public class PlaybackControlView extends FrameLayout { /** * Sets the {@link ExoPlayer} to control. * - * @param player the {@code ExoPlayer} to control. + * @param player The {@code ExoPlayer} to control. */ public void setPlayer(ExoPlayer player) { if (this.player == player) { @@ -330,6 +363,19 @@ public class PlaybackControlView extends FrameLayout { updateAll(); } + /** + * Sets whether the time bar should show all windows, as opposed to just the current one. If the + * timeline has a period with unknown duration or more than + * {@link #MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR} windows the time bar will fall back to showing a + * single window. + * + * @param showMultiWindowTimeBar Whether the time bar should show all windows. + */ + public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) { + this.showMultiWindowTimeBar = showMultiWindowTimeBar; + updateTimeBarMode(); + } + /** * Sets the {@link VisibilityListener}. * @@ -340,13 +386,14 @@ public class PlaybackControlView extends FrameLayout { } /** - * Sets the {@link SeekDispatcher}. + * Sets the {@link ControlDispatcher}. * - * @param seekDispatcher The {@link SeekDispatcher}, or null to use - * {@link #DEFAULT_SEEK_DISPATCHER}. + * @param controlDispatcher The {@link ControlDispatcher}, or null to use + * {@link #DEFAULT_CONTROL_DISPATCHER}. */ - public void setSeekDispatcher(SeekDispatcher seekDispatcher) { - this.seekDispatcher = seekDispatcher == null ? DEFAULT_SEEK_DISPATCHER : seekDispatcher; + public void setControlDispatcher(ControlDispatcher controlDispatcher) { + this.controlDispatcher = controlDispatcher == null ? DEFAULT_CONTROL_DISPATCHER + : controlDispatcher; } /** @@ -473,51 +520,117 @@ public class PlaybackControlView extends FrameLayout { if (!isVisible() || !isAttachedToWindow) { return; } - Timeline currentTimeline = player != null ? player.getCurrentTimeline() : null; - boolean haveNonEmptyTimeline = currentTimeline != null && !currentTimeline.isEmpty(); + Timeline timeline = player != null ? player.getCurrentTimeline() : null; + boolean haveNonEmptyTimeline = timeline != null && !timeline.isEmpty(); boolean isSeekable = false; boolean enablePrevious = false; boolean enableNext = false; if (haveNonEmptyTimeline) { - int currentWindowIndex = player.getCurrentWindowIndex(); - currentTimeline.getWindow(currentWindowIndex, currentWindow); - isSeekable = currentWindow.isSeekable; - enablePrevious = currentWindowIndex > 0 || isSeekable || !currentWindow.isDynamic; - enableNext = (currentWindowIndex < currentTimeline.getWindowCount() - 1) - || currentWindow.isDynamic; + int windowIndex = player.getCurrentWindowIndex(); + timeline.getWindow(windowIndex, window); + isSeekable = window.isSeekable; + enablePrevious = windowIndex > 0 || isSeekable || !window.isDynamic; + enableNext = (windowIndex < timeline.getWindowCount() - 1) || window.isDynamic; + if (timeline.getPeriod(player.getCurrentPeriodIndex(), period).isAd) { + // Always hide player controls during ads. + hide(); + } } - setButtonEnabled(enablePrevious , previousButton); + setButtonEnabled(enablePrevious, previousButton); setButtonEnabled(enableNext, nextButton); setButtonEnabled(fastForwardMs > 0 && isSeekable, fastForwardButton); setButtonEnabled(rewindMs > 0 && isSeekable, rewindButton); - if (progressBar != null) { - progressBar.setEnabled(isSeekable); + if (timeBar != null) { + timeBar.setEnabled(isSeekable); } } + private void updateTimeBarMode() { + if (player == null) { + return; + } + multiWindowTimeBar = showMultiWindowTimeBar + && canShowMultiWindowTimeBar(player.getCurrentTimeline(), period); + } + private void updateProgress() { if (!isVisible() || !isAttachedToWindow) { return; } - long duration = player == null ? 0 : player.getDuration(); - long position = player == null ? 0 : player.getCurrentPosition(); - if (durationView != null) { - durationView.setText(stringForTime(duration)); + + long position = 0; + long bufferedPosition = 0; + long duration = 0; + if (player != null) { + if (multiWindowTimeBar) { + Timeline timeline = player.getCurrentTimeline(); + int windowCount = timeline.getWindowCount(); + int periodIndex = player.getCurrentPeriodIndex(); + long positionUs = 0; + long bufferedPositionUs = 0; + long durationUs = 0; + boolean isInAdBreak = false; + boolean isPlayingAd = false; + int adBreakCount = 0; + for (int i = 0; i < windowCount; i++) { + timeline.getWindow(i, window); + for (int j = window.firstPeriodIndex; j <= window.lastPeriodIndex; j++) { + if (timeline.getPeriod(j, period).isAd) { + isPlayingAd |= j == periodIndex; + if (!isInAdBreak) { + isInAdBreak = true; + if (adBreakCount == adBreakTimesMs.length) { + adBreakTimesMs = Arrays.copyOf(adBreakTimesMs, + adBreakTimesMs.length == 0 ? 1 : adBreakTimesMs.length * 2); + } + adBreakTimesMs[adBreakCount++] = C.usToMs(durationUs); + } + } else { + isInAdBreak = false; + long periodDurationUs = period.getDurationUs(); + Assertions.checkState(periodDurationUs != C.TIME_UNSET); + long periodDurationInWindowUs = periodDurationUs; + if (j == window.firstPeriodIndex) { + periodDurationInWindowUs -= window.positionInFirstPeriodUs; + } + if (i < periodIndex) { + positionUs += periodDurationInWindowUs; + bufferedPositionUs += periodDurationInWindowUs; + } + durationUs += periodDurationInWindowUs; + } + } + } + position = C.usToMs(positionUs); + bufferedPosition = C.usToMs(bufferedPositionUs); + duration = C.usToMs(durationUs); + if (!isPlayingAd) { + position += player.getCurrentPosition(); + bufferedPosition += player.getBufferedPosition(); + } + if (timeBar != null) { + timeBar.setAdBreakTimesMs(adBreakTimesMs, adBreakCount); + } + } else { + position = player.getCurrentPosition(); + bufferedPosition = player.getBufferedPosition(); + duration = player.getDuration(); + } } - if (positionView != null && !dragging) { - positionView.setText(stringForTime(position)); + if (durationView != null) { + durationView.setText(Util.getStringForTime(formatBuilder, formatter, duration)); + } + if (positionView != null && !scrubbing) { + positionView.setText(Util.getStringForTime(formatBuilder, formatter, position)); + } + if (timeBar != null) { + timeBar.setPosition(position); + timeBar.setBufferedPosition(bufferedPosition); + timeBar.setDuration(duration); } - if (progressBar != null) { - if (!dragging) { - progressBar.setProgress(progressBarValue(position)); - } - long bufferedPosition = player == null ? 0 : player.getBufferedPosition(); - progressBar.setSecondaryProgress(progressBarValue(bufferedPosition)); - // Remove scheduled updates. - } + // Cancel any pending updates and schedule a new one if necessary. removeCallbacks(updateProgressAction); - // Schedule an update if necessary. int playbackState = player == null ? ExoPlayer.STATE_IDLE : player.getPlaybackState(); if (playbackState != ExoPlayer.STATE_IDLE && playbackState != ExoPlayer.STATE_ENDED) { long delayMs; @@ -560,55 +673,31 @@ public class PlaybackControlView extends FrameLayout { view.setAlpha(alpha); } - private String stringForTime(long timeMs) { - if (timeMs == C.TIME_UNSET) { - timeMs = 0; - } - long totalSeconds = (timeMs + 500) / 1000; - long seconds = totalSeconds % 60; - long minutes = (totalSeconds / 60) % 60; - long hours = totalSeconds / 3600; - formatBuilder.setLength(0); - return hours > 0 ? formatter.format("%d:%02d:%02d", hours, minutes, seconds).toString() - : formatter.format("%02d:%02d", minutes, seconds).toString(); - } - - private int progressBarValue(long position) { - long duration = player == null ? C.TIME_UNSET : player.getDuration(); - return duration == C.TIME_UNSET || duration == 0 ? 0 - : (int) ((position * PROGRESS_BAR_MAX) / duration); - } - - private long positionValue(int progress) { - long duration = player == null ? C.TIME_UNSET : player.getDuration(); - return duration == C.TIME_UNSET ? 0 : ((duration * progress) / PROGRESS_BAR_MAX); - } - private void previous() { - Timeline currentTimeline = player.getCurrentTimeline(); - if (currentTimeline.isEmpty()) { + Timeline timeline = player.getCurrentTimeline(); + if (timeline.isEmpty()) { return; } - int currentWindowIndex = player.getCurrentWindowIndex(); - currentTimeline.getWindow(currentWindowIndex, currentWindow); - if (currentWindowIndex > 0 && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS - || (currentWindow.isDynamic && !currentWindow.isSeekable))) { - seekTo(currentWindowIndex - 1, C.TIME_UNSET); + int windowIndex = player.getCurrentWindowIndex(); + timeline.getWindow(windowIndex, window); + if (windowIndex > 0 && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS + || (window.isDynamic && !window.isSeekable))) { + seekTo(windowIndex - 1, C.TIME_UNSET); } else { seekTo(0); } } private void next() { - Timeline currentTimeline = player.getCurrentTimeline(); - if (currentTimeline.isEmpty()) { + Timeline timeline = player.getCurrentTimeline(); + if (timeline.isEmpty()) { return; } - int currentWindowIndex = player.getCurrentWindowIndex(); - if (currentWindowIndex < currentTimeline.getWindowCount() - 1) { - seekTo(currentWindowIndex + 1, C.TIME_UNSET); - } else if (currentTimeline.getWindow(currentWindowIndex, currentWindow, false).isDynamic) { - seekTo(currentWindowIndex, C.TIME_UNSET); + int windowIndex = player.getCurrentWindowIndex(); + if (windowIndex < timeline.getWindowCount() - 1) { + seekTo(windowIndex + 1, C.TIME_UNSET); + } else if (timeline.getWindow(windowIndex, window, false).isDynamic) { + seekTo(windowIndex, C.TIME_UNSET); } } @@ -631,7 +720,7 @@ public class PlaybackControlView extends FrameLayout { } private void seekTo(int windowIndex, long positionMs) { - boolean dispatched = seekDispatcher.dispatchSeek(player, windowIndex, positionMs); + boolean dispatched = controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs); if (!dispatched) { // The seek wasn't dispatched. If the progress bar was dragged by the user to perform the // seek then it'll now be in the wrong position. Trigger a progress update to snap it back. @@ -639,6 +728,42 @@ public class PlaybackControlView extends FrameLayout { } } + private void seekToTimebarPosition(long timebarPositionMs) { + if (multiWindowTimeBar) { + Timeline timeline = player.getCurrentTimeline(); + int windowCount = timeline.getWindowCount(); + long remainingMs = timebarPositionMs; + for (int i = 0; i < windowCount; i++) { + timeline.getWindow(i, window); + for (int j = window.firstPeriodIndex; j <= window.lastPeriodIndex; j++) { + if (!timeline.getPeriod(j, period).isAd) { + long periodDurationMs = period.getDurationMs(); + if (periodDurationMs == C.TIME_UNSET) { + // Should never happen as canShowMultiWindowTimeBar is true. + throw new IllegalStateException(); + } + if (j == window.firstPeriodIndex) { + periodDurationMs -= window.getPositionInFirstPeriodMs(); + } + if (i == windowCount - 1 && j == window.lastPeriodIndex + && remainingMs >= periodDurationMs) { + // Seeking past the end of the last window should seek to the end of the timeline. + seekTo(i, window.getDurationMs()); + return; + } + if (remainingMs < periodDurationMs) { + seekTo(i, period.getPositionInWindowMs() + remainingMs); + return; + } + remainingMs -= periodDurationMs; + } + } + } + } else { + seekTo(timebarPositionMs); + } + } + @Override public void onAttachedToWindow() { super.onAttachedToWindow(); @@ -692,13 +817,13 @@ public class PlaybackControlView extends FrameLayout { rewind(); break; case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: - player.setPlayWhenReady(!player.getPlayWhenReady()); + controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady()); break; case KeyEvent.KEYCODE_MEDIA_PLAY: - player.setPlayWhenReady(true); + controlDispatcher.dispatchSetPlayWhenReady(player, true); break; case KeyEvent.KEYCODE_MEDIA_PAUSE: - player.setPlayWhenReady(false); + controlDispatcher.dispatchSetPlayWhenReady(player, false); break; case KeyEvent.KEYCODE_MEDIA_NEXT: next(); @@ -714,6 +839,7 @@ public class PlaybackControlView extends FrameLayout { return true; } + @SuppressLint("InlinedApi") private static boolean isHandledMediaKey(int keyCode) { return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD || keyCode == KeyEvent.KEYCODE_MEDIA_REWIND @@ -724,33 +850,48 @@ public class PlaybackControlView extends FrameLayout { || keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS; } - private final class ComponentListener implements ExoPlayer.EventListener, - SeekBar.OnSeekBarChangeListener, OnClickListener { + /** + * Returns whether the specified {@code timeline} can be shown on a multi-window time bar. + * + * @param timeline The {@link Timeline} to check. + * @param period A scratch {@link Timeline.Period} instance. + * @return Whether the specified timeline can be shown on a multi-window time bar. + */ + private static boolean canShowMultiWindowTimeBar(Timeline timeline, Timeline.Period period) { + if (timeline.getWindowCount() > MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR) { + return false; + } + int periodCount = timeline.getPeriodCount(); + for (int i = 0; i < periodCount; i++) { + timeline.getPeriod(i, period); + if (!period.isAd && period.durationUs == C.TIME_UNSET) { + return false; + } + } + return true; + } + + private final class ComponentListener implements ExoPlayer.EventListener, TimeBar.OnScrubListener, + OnClickListener { @Override - public void onStartTrackingTouch(SeekBar seekBar) { + public void onScrubStart(TimeBar timeBar) { removeCallbacks(hideAction); - dragging = true; + scrubbing = true; } @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - if (fromUser) { - long position = positionValue(progress); - if (positionView != null) { - positionView.setText(stringForTime(position)); - } - if (player != null && !dragging) { - seekTo(position); - } + public void onScrubMove(TimeBar timeBar, long position) { + if (positionView != null) { + positionView.setText(Util.getStringForTime(formatBuilder, formatter, position)); } } @Override - public void onStopTrackingTouch(SeekBar seekBar) { - dragging = false; - if (player != null) { - seekTo(positionValue(seekBar.getProgress())); + public void onScrubStop(TimeBar timeBar, long position, boolean canceled) { + scrubbing = false; + if (!canceled && player != null) { + seekToTimebarPosition(position); } hideAfterTimeout(); } @@ -767,9 +908,15 @@ public class PlaybackControlView extends FrameLayout { updateProgress(); } + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + // Do nothing. + } + @Override public void onTimelineChanged(Timeline timeline, Object manifest) { updateNavigation(); + updateTimeBarMode(); updateProgress(); } @@ -800,9 +947,9 @@ public class PlaybackControlView extends FrameLayout { } else if (rewindButton == view) { rewind(); } else if (playButton == view) { - player.setPlayWhenReady(true); + controlDispatcher.dispatchSetPlayWhenReady(player, true); } else if (pauseButton == view) { - player.setPlayWhenReady(false); + controlDispatcher.dispatchSetPlayWhenReady(player, false); } } hideAfterTimeout(); diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java similarity index 81% rename from library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java rename to library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 3349e05eda..fce05f5bc4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -17,9 +17,12 @@ package com.google.android.exoplayer2.ui; import android.annotation.TargetApi; import android.content.Context; +import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -33,7 +36,7 @@ import android.widget.ImageView; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.R; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.metadata.Metadata; @@ -44,8 +47,9 @@ import com.google.android.exoplayer2.text.TextRenderer; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode; -import com.google.android.exoplayer2.ui.PlaybackControlView.SeekDispatcher; +import com.google.android.exoplayer2.ui.PlaybackControlView.ControlDispatcher; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; import java.util.List; /** @@ -72,12 +76,18 @@ import java.util.List; *

  • Default: {@code null}
  • * * - *
  • {@code use_controller} - Whether playback controls are displayed. + *
  • {@code use_controller} - Whether the playback controls can be shown. *
      *
    • Corresponding method: {@link #setUseController(boolean)}
    • *
    • Default: {@code true}
    • *
    *
  • + *
  • {@code hide_on_touch} - Whether the playback controls are hidden by touch events. + *
      + *
    • Corresponding method: {@link #setControllerHideOnTouch(boolean)}
    • + *
    • Default: {@code true}
    • + *
    + *
  • *
  • {@code resize_mode} - Controls how video and album art is resized within the view. * Valid values are {@code fit}, {@code fixed_width}, {@code fixed_height} and {@code fill}. *
      @@ -188,6 +198,7 @@ public final class SimpleExoPlayerView extends FrameLayout { private boolean useArtwork; private Bitmap defaultArtwork; private int controllerShowTimeoutMs; + private boolean controllerHideOnTouch; public SimpleExoPlayerView(Context context) { this(context, null); @@ -200,6 +211,25 @@ public final class SimpleExoPlayerView extends FrameLayout { public SimpleExoPlayerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); + if (isInEditMode()) { + contentFrame = null; + shutterView = null; + surfaceView = null; + artworkView = null; + subtitleView = null; + controller = null; + componentListener = null; + overlayFrameLayout = null; + ImageView logo = new ImageView(context, attrs); + if (Util.SDK_INT >= 23) { + configureEditModeLogoV23(getResources(), logo); + } else { + configureEditModeLogo(getResources(), logo); + } + addView(logo); + return; + } + int playerLayoutId = R.layout.exo_simple_player_view; boolean useArtwork = true; int defaultArtworkId = 0; @@ -207,6 +237,7 @@ public final class SimpleExoPlayerView extends FrameLayout { int surfaceType = SURFACE_TYPE_SURFACE_VIEW; int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; int controllerShowTimeoutMs = PlaybackControlView.DEFAULT_SHOW_TIMEOUT_MS; + boolean controllerHideOnTouch = true; if (attrs != null) { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SimpleExoPlayerView, 0, 0); @@ -221,6 +252,8 @@ public final class SimpleExoPlayerView extends FrameLayout { resizeMode = a.getInt(R.styleable.SimpleExoPlayerView_resize_mode, resizeMode); controllerShowTimeoutMs = a.getInt(R.styleable.SimpleExoPlayerView_show_timeout, controllerShowTimeoutMs); + controllerHideOnTouch = a.getBoolean(R.styleable.SimpleExoPlayerView_hide_on_touch, + controllerHideOnTouch); } finally { a.recycle(); } @@ -283,10 +316,35 @@ public final class SimpleExoPlayerView extends FrameLayout { this.controller = null; } this.controllerShowTimeoutMs = controller != null ? controllerShowTimeoutMs : 0; + this.controllerHideOnTouch = controllerHideOnTouch; this.useController = useController && controller != null; hideController(); } + /** + * Switches the view targeted by a given {@link SimpleExoPlayer}. + * + * @param player The player whose target view is being switched. + * @param oldPlayerView The old view to detach from the player. + * @param newPlayerView The new view to attach to the player. + */ + public static void switchTargetView(@NonNull SimpleExoPlayer player, + @Nullable SimpleExoPlayerView oldPlayerView, @Nullable SimpleExoPlayerView newPlayerView) { + if (oldPlayerView == newPlayerView) { + return; + } + // We attach the new view before detaching the old one because this ordering allows the player + // to swap directly from one surface to another, without transitioning through a state where no + // surface is attached. This is significantly more efficient and achieves a more seamless + // transition when using platform provided video decoders. + if (newPlayerView != null) { + newPlayerView.setPlayer(player); + } + if (oldPlayerView != null) { + oldPlayerView.setPlayer(null); + } + } + /** * Returns the player currently set on this view, or null if no player is set. */ @@ -298,6 +356,12 @@ public final class SimpleExoPlayerView extends FrameLayout { * Set the {@link SimpleExoPlayer} to use. The {@link SimpleExoPlayer#setTextOutput} and * {@link SimpleExoPlayer#setVideoListener} method of the player will be called and previous * assignments are overridden. + *

      + * To transition a {@link SimpleExoPlayer} from targeting one view to another, it's recommended to + * use {@link #switchTargetView(SimpleExoPlayer, SimpleExoPlayerView, SimpleExoPlayerView)} rather + * than this method. If you do wish to use this method directly, be sure to attach the player to + * the new view before calling {@code setPlayer(null)} to detach it from the old one. + * This ordering is significantly more efficient and may allow for more seamless transitions. * * @param player The {@link SimpleExoPlayer} to use. */ @@ -306,10 +370,14 @@ public final class SimpleExoPlayerView extends FrameLayout { return; } if (this.player != null) { - this.player.setTextOutput(null); - this.player.setVideoListener(null); this.player.removeListener(componentListener); - this.player.setVideoSurface(null); + this.player.clearTextOutput(componentListener); + this.player.clearVideoListener(componentListener); + if (surfaceView instanceof TextureView) { + this.player.clearVideoTextureView((TextureView) surfaceView); + } else if (surfaceView instanceof SurfaceView) { + this.player.clearVideoSurfaceView((SurfaceView) surfaceView); + } } this.player = player; if (useController) { @@ -325,8 +393,8 @@ public final class SimpleExoPlayerView extends FrameLayout { player.setVideoSurfaceView((SurfaceView) surfaceView); } player.setVideoListener(componentListener); - player.addListener(componentListener); player.setTextOutput(componentListener); + player.addListener(componentListener); maybeShowController(false); updateForCurrentTrackSelections(); } else { @@ -386,17 +454,17 @@ public final class SimpleExoPlayerView extends FrameLayout { } /** - * Returns whether the playback controls are enabled. + * Returns whether the playback controls can be shown. */ public boolean getUseController() { return useController; } /** - * Sets whether playback controls are enabled. If set to {@code false} the playback controls are - * never visible and are disconnected from the player. + * Sets whether the playback controls can be shown. If set to {@code false} the playback controls + * are never visible and are disconnected from the player. * - * @param useController Whether playback controls should be enabled. + * @param useController Whether the playback controls can be shown. */ public void setUseController(boolean useController) { Assertions.checkState(!useController || controller != null); @@ -465,6 +533,23 @@ public final class SimpleExoPlayerView extends FrameLayout { this.controllerShowTimeoutMs = controllerShowTimeoutMs; } + /** + * Returns whether the playback controls are hidden by touch events. + */ + public boolean getControllerHideOnTouch() { + return controllerHideOnTouch; + } + + /** + * Sets whether the playback controls are hidden by touch events. + * + * @param controllerHideOnTouch Whether the playback controls are hidden by touch events. + */ + public void setControllerHideOnTouch(boolean controllerHideOnTouch) { + Assertions.checkState(controller != null); + this.controllerHideOnTouch = controllerHideOnTouch; + } + /** * Set the {@link PlaybackControlView.VisibilityListener}. * @@ -476,14 +561,14 @@ public final class SimpleExoPlayerView extends FrameLayout { } /** - * Sets the {@link SeekDispatcher}. + * Sets the {@link ControlDispatcher}. * - * @param seekDispatcher The {@link SeekDispatcher}, or null to use - * {@link PlaybackControlView#DEFAULT_SEEK_DISPATCHER}. + * @param controlDispatcher The {@link ControlDispatcher}, or null to use + * {@link PlaybackControlView#DEFAULT_CONTROL_DISPATCHER}. */ - public void setSeekDispatcher(SeekDispatcher seekDispatcher) { + public void setControlDispatcher(ControlDispatcher controlDispatcher) { Assertions.checkState(controller != null); - controller.setSeekDispatcher(seekDispatcher); + controller.setControlDispatcher(controlDispatcher); } /** @@ -506,6 +591,16 @@ public final class SimpleExoPlayerView extends FrameLayout { controller.setFastForwardIncrementMs(fastForwardMs); } + /** + * Sets whether the time bar should show all windows, as opposed to just the current one. + * + * @param showMultiWindowTimeBar Whether to show all windows. + */ + public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) { + Assertions.checkState(controller != null); + controller.setShowMultiWindowTimeBar(showMultiWindowTimeBar); + } + /** * Gets the view onto which video is rendered. This is either a {@link SurfaceView} (default) * or a {@link TextureView} if the {@code use_texture_view} view attribute has been set to true. @@ -542,10 +637,10 @@ public final class SimpleExoPlayerView extends FrameLayout { if (!useController || player == null || ev.getActionMasked() != MotionEvent.ACTION_DOWN) { return false; } - if (controller.isVisible()) { - controller.hide(); - } else { + if (!controller.isVisible()) { maybeShowController(true); + } else if (controllerHideOnTouch) { + controller.hide(); } return true; } @@ -646,6 +741,19 @@ public final class SimpleExoPlayerView extends FrameLayout { } } + @TargetApi(23) + private static void configureEditModeLogoV23(Resources resources, ImageView logo) { + logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo, null)); + logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color, null)); + } + + @SuppressWarnings("deprecation") + private static void configureEditModeLogo(Resources resources, ImageView logo) { + logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo)); + logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color)); + } + + @SuppressWarnings("ResourceType") private static void setResizeModeRaw(AspectRatioFrameLayout aspectRatioFrame, int resizeMode) { aspectRatioFrame.setResizeMode(resizeMode); @@ -708,6 +816,11 @@ public final class SimpleExoPlayerView extends FrameLayout { // Do nothing. } + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + // Do nothing. + } + @Override public void onTimelineChanged(Timeline timeline, Object manifest) { // Do nothing. diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java similarity index 86% rename from library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java rename to library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index d4f09b1721..b6cfc9a6f3 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -27,9 +27,12 @@ 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; @@ -77,7 +80,9 @@ import com.google.android.exoplayer2.util.Util; @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; @@ -132,6 +137,8 @@ import com.google.android.exoplayer2.util.Util; * * @param cue The cue to draw. * @param applyEmbeddedStyles Whether styling embedded within the cue should be applied. + * @param applyEmbeddedFontSizes If {@code applyEmbeddedStyles} is true, defines whether font + * sizes embedded within the cue should be applied. Otherwise, it is ignored. * @param style The style to use when drawing the cue text. * @param textSizePx The text size to use when drawing the cue text, in pixels. * @param bottomPaddingFraction The bottom padding fraction to apply when {@link Cue#line} is @@ -142,38 +149,31 @@ import com.google.android.exoplayer2.util.Util; * @param cueBoxRight The right position of the enclosing cue box. * @param cueBoxBottom The bottom position of the enclosing cue box. */ - public void draw(Cue cue, boolean applyEmbeddedStyles, CaptionStyleCompat style, float textSizePx, - float bottomPaddingFraction, Canvas canvas, int cueBoxLeft, int cueBoxTop, int cueBoxRight, - int cueBoxBottom) { + public void draw(Cue cue, boolean applyEmbeddedStyles, boolean applyEmbeddedFontSizes, + CaptionStyleCompat style, float textSizePx, float bottomPaddingFraction, Canvas canvas, + int cueBoxLeft, int cueBoxTop, int cueBoxRight, int cueBoxBottom) { boolean isTextCue = cue.bitmap == null; - CharSequence cueText = null; - Bitmap cueBitmap = null; int windowColor = Color.BLACK; if (isTextCue) { - cueText = cue.text; - if (TextUtils.isEmpty(cueText)) { + if (TextUtils.isEmpty(cue.text)) { // Nothing to draw. return; } - windowColor = cue.windowColorSet ? cue.windowColor : style.windowColor; - if (!applyEmbeddedStyles) { - // Strip out any embedded styling. - cueText = cueText.toString(); - windowColor = style.windowColor; - } - } else { - cueBitmap = cue.bitmap; + windowColor = (cue.windowColorSet && applyEmbeddedStyles) + ? cue.windowColor : style.windowColor; } - if (areCharSequencesEqual(this.cueText, cueText) + if (areCharSequencesEqual(this.cueText, cue.text) && Util.areEqual(this.cueTextAlignment, cue.textAlignment) - && this.cueBitmap == cueBitmap + && this.cueBitmap == cue.bitmap && this.cueLine == cue.line && this.cueLineType == cue.lineType && Util.areEqual(this.cueLineAnchor, cue.lineAnchor) && this.cuePosition == cue.position && Util.areEqual(this.cuePositionAnchor, cue.positionAnchor) && this.cueSize == cue.size + && this.cueBitmapHeight == cue.bitmapHeight && this.applyEmbeddedStyles == applyEmbeddedStyles + && this.applyEmbeddedFontSizes == applyEmbeddedFontSizes && this.foregroundColor == style.foregroundColor && this.backgroundColor == style.backgroundColor && this.windowColor == windowColor @@ -191,16 +191,18 @@ import com.google.android.exoplayer2.util.Util; return; } - this.cueText = cueText; + this.cueText = cue.text; this.cueTextAlignment = cue.textAlignment; - this.cueBitmap = cueBitmap; + 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 = style.foregroundColor; this.backgroundColor = style.backgroundColor; this.windowColor = windowColor; @@ -238,6 +240,26 @@ import com.google.android.exoplayer2.util.Util; 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); @@ -312,7 +334,8 @@ import com.google.android.exoplayer2.util.Util; float anchorX = parentLeft + (parentWidth * cuePosition); float anchorY = parentTop + (parentHeight * cueLine); int width = Math.round(parentWidth * cueSize); - int height = Math.round(width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth())); + int height = cueBitmapHeight != Cue.DIMEN_UNSET ? 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 / 2)) : anchorX); int y = Math.round(cuePositionAnchor == Cue.ANCHOR_TYPE_END ? (anchorY - height) diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java similarity index 88% rename from library/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java rename to library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java index 49516ab6f4..3bcfcc3ef3 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java @@ -60,6 +60,7 @@ public final class SubtitleView extends View implements TextRenderer.Output { private int textSizeType; private float textSize; private boolean applyEmbeddedStyles; + private boolean applyEmbeddedFontSizes; private CaptionStyleCompat style; private float bottomPaddingFraction; @@ -73,6 +74,7 @@ public final class SubtitleView extends View implements TextRenderer.Output { textSizeType = FRACTIONAL; textSize = DEFAULT_TEXT_SIZE_FRACTION; applyEmbeddedStyles = true; + applyEmbeddedFontSizes = true; style = CaptionStyleCompat.DEFAULT; bottomPaddingFraction = DEFAULT_BOTTOM_PADDING_FRACTION; } @@ -166,14 +168,32 @@ public final class SubtitleView extends View implements TextRenderer.Output { /** * Sets whether styling embedded within the cues should be applied. Enabled by default. + * Overrides any setting made with {@link SubtitleView#setApplyEmbeddedFontSizes}. * * @param applyEmbeddedStyles Whether styling embedded within the cues should be applied. */ public void setApplyEmbeddedStyles(boolean applyEmbeddedStyles) { - if (this.applyEmbeddedStyles == applyEmbeddedStyles) { + if (this.applyEmbeddedStyles == applyEmbeddedStyles + && this.applyEmbeddedFontSizes == applyEmbeddedStyles) { return; } this.applyEmbeddedStyles = applyEmbeddedStyles; + this.applyEmbeddedFontSizes = applyEmbeddedStyles; + // Invalidate to trigger drawing. + invalidate(); + } + + /** + * Sets whether font sizes embedded within the cues should be applied. Enabled by default. + * Only takes effect if {@link SubtitleView#setApplyEmbeddedStyles} is set to true. + * + * @param applyEmbeddedFontSizes Whether font sizes embedded within the cues should be applied. + */ + public void setApplyEmbeddedFontSizes(boolean applyEmbeddedFontSizes) { + if (this.applyEmbeddedFontSizes == applyEmbeddedFontSizes) { + return; + } + this.applyEmbeddedFontSizes = applyEmbeddedFontSizes; // Invalidate to trigger drawing. invalidate(); } @@ -243,8 +263,8 @@ public final class SubtitleView extends View implements TextRenderer.Output { } for (int i = 0; i < cueCount; i++) { - painters.get(i).draw(cues.get(i), applyEmbeddedStyles, style, textSizePx, - bottomPaddingFraction, canvas, left, top, right, bottom); + painters.get(i).draw(cues.get(i), applyEmbeddedStyles, applyEmbeddedFontSizes, style, + textSizePx, bottomPaddingFraction, canvas, left, top, right, bottom); } } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java new file mode 100644 index 0000000000..aeb8e0255e --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java @@ -0,0 +1,120 @@ +/* + * 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.google.android.exoplayer2.ui; + +import android.support.annotation.Nullable; +import android.view.View; + +/** + * Interface for time bar views that can display a playback position, buffered position, duration + * and ad markers, and that have a listener for scrubbing (seeking) events. + */ +public interface TimeBar { + + /** + * @see View#isEnabled() + */ + void setEnabled(boolean enabled); + + /** + * Sets the listener for the scrubbing events. + * + * @param listener The listener for scrubbing events. + */ + void setListener(OnScrubListener listener); + + /** + * Sets the position increment for key presses and accessibility actions, in milliseconds. + *

      + * Clears any increment specified in a preceding call to {@link #setKeyCountIncrement(int)}. + * + * @param time The time increment, in milliseconds. + */ + void setKeyTimeIncrement(long time); + + /** + * Sets the position increment for key presses and accessibility actions, as a number of + * increments that divide the duration of the media. For example, passing 20 will cause key + * presses to increment/decrement the position by 1/20th of the duration (if known). + *

      + * Clears any increment specified in a preceding call to {@link #setKeyTimeIncrement(long)}. + * + * @param count The number of increments that divide the duration of the media. + */ + void setKeyCountIncrement(int count); + + /** + * Sets the current position. + * + * @param position The current position to show, in milliseconds. + */ + void setPosition(long position); + + /** + * Sets the buffered position. + * + * @param bufferedPosition The current buffered position to show, in milliseconds. + */ + void setBufferedPosition(long bufferedPosition); + + /** + * Sets the duration. + * + * @param duration The duration to show, in milliseconds. + */ + void setDuration(long duration); + + /** + * Sets the times of ad breaks. + * + * @param adBreakTimesMs An array where the first {@code adBreakCount} elements are the times of + * ad breaks in milliseconds. May be {@code null} if there are no ad breaks. + * @param adBreakCount The number of ad breaks. + */ + void setAdBreakTimesMs(@Nullable long[] adBreakTimesMs, int adBreakCount); + + /** + * Listener for scrubbing events. + */ + interface OnScrubListener { + + /** + * Called when the user starts moving the scrubber. + * + * @param timeBar The time bar. + */ + void onScrubStart(TimeBar timeBar); + + /** + * Called when the user moves the scrubber. + * + * @param timeBar The time bar. + * @param position The position of the scrubber, in milliseconds. + */ + void onScrubMove(TimeBar timeBar, long position); + + /** + * Called when the user stops moving the scrubber. + * + * @param timeBar The time bar. + * @param position The position of the scrubber, in milliseconds. + * @param canceled Whether scrubbing was canceled. + */ + void onScrubStop(TimeBar timeBar, long position, boolean canceled); + + } + +} diff --git a/library/src/main/res/drawable-anydpi-v21/exo_controls_fastforward.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_fastforward.xml similarity index 100% rename from library/src/main/res/drawable-anydpi-v21/exo_controls_fastforward.xml rename to library/ui/src/main/res/drawable-anydpi-v21/exo_controls_fastforward.xml diff --git a/library/src/main/res/drawable-anydpi-v21/exo_controls_next.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_next.xml similarity index 100% rename from library/src/main/res/drawable-anydpi-v21/exo_controls_next.xml rename to library/ui/src/main/res/drawable-anydpi-v21/exo_controls_next.xml diff --git a/library/src/main/res/drawable-anydpi-v21/exo_controls_pause.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_pause.xml similarity index 100% rename from library/src/main/res/drawable-anydpi-v21/exo_controls_pause.xml rename to library/ui/src/main/res/drawable-anydpi-v21/exo_controls_pause.xml diff --git a/library/src/main/res/drawable-anydpi-v21/exo_controls_play.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_play.xml similarity index 100% rename from library/src/main/res/drawable-anydpi-v21/exo_controls_play.xml rename to library/ui/src/main/res/drawable-anydpi-v21/exo_controls_play.xml diff --git a/library/src/main/res/drawable-anydpi-v21/exo_controls_previous.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_previous.xml similarity index 100% rename from library/src/main/res/drawable-anydpi-v21/exo_controls_previous.xml rename to library/ui/src/main/res/drawable-anydpi-v21/exo_controls_previous.xml diff --git a/library/src/main/res/drawable-anydpi-v21/exo_controls_rewind.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_rewind.xml similarity index 100% rename from library/src/main/res/drawable-anydpi-v21/exo_controls_rewind.xml rename to library/ui/src/main/res/drawable-anydpi-v21/exo_controls_rewind.xml diff --git a/library/src/main/res/drawable-hdpi/exo_controls_fastforward.png b/library/ui/src/main/res/drawable-hdpi/exo_controls_fastforward.png similarity index 100% rename from library/src/main/res/drawable-hdpi/exo_controls_fastforward.png rename to library/ui/src/main/res/drawable-hdpi/exo_controls_fastforward.png diff --git a/library/src/main/res/drawable-hdpi/exo_controls_next.png b/library/ui/src/main/res/drawable-hdpi/exo_controls_next.png similarity index 100% rename from library/src/main/res/drawable-hdpi/exo_controls_next.png rename to library/ui/src/main/res/drawable-hdpi/exo_controls_next.png diff --git a/library/src/main/res/drawable-hdpi/exo_controls_pause.png b/library/ui/src/main/res/drawable-hdpi/exo_controls_pause.png similarity index 100% rename from library/src/main/res/drawable-hdpi/exo_controls_pause.png rename to library/ui/src/main/res/drawable-hdpi/exo_controls_pause.png diff --git a/library/src/main/res/drawable-hdpi/exo_controls_play.png b/library/ui/src/main/res/drawable-hdpi/exo_controls_play.png similarity index 100% rename from library/src/main/res/drawable-hdpi/exo_controls_play.png rename to library/ui/src/main/res/drawable-hdpi/exo_controls_play.png diff --git a/library/src/main/res/drawable-hdpi/exo_controls_previous.png b/library/ui/src/main/res/drawable-hdpi/exo_controls_previous.png similarity index 100% rename from library/src/main/res/drawable-hdpi/exo_controls_previous.png rename to library/ui/src/main/res/drawable-hdpi/exo_controls_previous.png diff --git a/library/src/main/res/drawable-hdpi/exo_controls_rewind.png b/library/ui/src/main/res/drawable-hdpi/exo_controls_rewind.png similarity index 100% rename from library/src/main/res/drawable-hdpi/exo_controls_rewind.png rename to library/ui/src/main/res/drawable-hdpi/exo_controls_rewind.png diff --git a/library/src/main/res/drawable-ldpi/exo_controls_fastforward.png b/library/ui/src/main/res/drawable-ldpi/exo_controls_fastforward.png similarity index 100% rename from library/src/main/res/drawable-ldpi/exo_controls_fastforward.png rename to library/ui/src/main/res/drawable-ldpi/exo_controls_fastforward.png diff --git a/library/src/main/res/drawable-ldpi/exo_controls_next.png b/library/ui/src/main/res/drawable-ldpi/exo_controls_next.png similarity index 100% rename from library/src/main/res/drawable-ldpi/exo_controls_next.png rename to library/ui/src/main/res/drawable-ldpi/exo_controls_next.png diff --git a/library/src/main/res/drawable-ldpi/exo_controls_pause.png b/library/ui/src/main/res/drawable-ldpi/exo_controls_pause.png similarity index 100% rename from library/src/main/res/drawable-ldpi/exo_controls_pause.png rename to library/ui/src/main/res/drawable-ldpi/exo_controls_pause.png diff --git a/library/src/main/res/drawable-ldpi/exo_controls_play.png b/library/ui/src/main/res/drawable-ldpi/exo_controls_play.png similarity index 100% rename from library/src/main/res/drawable-ldpi/exo_controls_play.png rename to library/ui/src/main/res/drawable-ldpi/exo_controls_play.png diff --git a/library/src/main/res/drawable-ldpi/exo_controls_previous.png b/library/ui/src/main/res/drawable-ldpi/exo_controls_previous.png similarity index 100% rename from library/src/main/res/drawable-ldpi/exo_controls_previous.png rename to library/ui/src/main/res/drawable-ldpi/exo_controls_previous.png diff --git a/library/src/main/res/drawable-ldpi/exo_controls_rewind.png b/library/ui/src/main/res/drawable-ldpi/exo_controls_rewind.png similarity index 100% rename from library/src/main/res/drawable-ldpi/exo_controls_rewind.png rename to library/ui/src/main/res/drawable-ldpi/exo_controls_rewind.png diff --git a/library/src/main/res/drawable-mdpi/exo_controls_fastforward.png b/library/ui/src/main/res/drawable-mdpi/exo_controls_fastforward.png similarity index 100% rename from library/src/main/res/drawable-mdpi/exo_controls_fastforward.png rename to library/ui/src/main/res/drawable-mdpi/exo_controls_fastforward.png diff --git a/library/src/main/res/drawable-mdpi/exo_controls_next.png b/library/ui/src/main/res/drawable-mdpi/exo_controls_next.png similarity index 100% rename from library/src/main/res/drawable-mdpi/exo_controls_next.png rename to library/ui/src/main/res/drawable-mdpi/exo_controls_next.png diff --git a/library/src/main/res/drawable-mdpi/exo_controls_pause.png b/library/ui/src/main/res/drawable-mdpi/exo_controls_pause.png similarity index 100% rename from library/src/main/res/drawable-mdpi/exo_controls_pause.png rename to library/ui/src/main/res/drawable-mdpi/exo_controls_pause.png diff --git a/library/src/main/res/drawable-mdpi/exo_controls_play.png b/library/ui/src/main/res/drawable-mdpi/exo_controls_play.png similarity index 100% rename from library/src/main/res/drawable-mdpi/exo_controls_play.png rename to library/ui/src/main/res/drawable-mdpi/exo_controls_play.png diff --git a/library/src/main/res/drawable-mdpi/exo_controls_previous.png b/library/ui/src/main/res/drawable-mdpi/exo_controls_previous.png similarity index 100% rename from library/src/main/res/drawable-mdpi/exo_controls_previous.png rename to library/ui/src/main/res/drawable-mdpi/exo_controls_previous.png diff --git a/library/src/main/res/drawable-mdpi/exo_controls_rewind.png b/library/ui/src/main/res/drawable-mdpi/exo_controls_rewind.png similarity index 100% rename from library/src/main/res/drawable-mdpi/exo_controls_rewind.png rename to library/ui/src/main/res/drawable-mdpi/exo_controls_rewind.png diff --git a/library/src/main/res/drawable-xhdpi/exo_controls_fastforward.png b/library/ui/src/main/res/drawable-xhdpi/exo_controls_fastforward.png similarity index 100% rename from library/src/main/res/drawable-xhdpi/exo_controls_fastforward.png rename to library/ui/src/main/res/drawable-xhdpi/exo_controls_fastforward.png diff --git a/library/src/main/res/drawable-xhdpi/exo_controls_next.png b/library/ui/src/main/res/drawable-xhdpi/exo_controls_next.png similarity index 100% rename from library/src/main/res/drawable-xhdpi/exo_controls_next.png rename to library/ui/src/main/res/drawable-xhdpi/exo_controls_next.png diff --git a/library/src/main/res/drawable-xhdpi/exo_controls_pause.png b/library/ui/src/main/res/drawable-xhdpi/exo_controls_pause.png similarity index 100% rename from library/src/main/res/drawable-xhdpi/exo_controls_pause.png rename to library/ui/src/main/res/drawable-xhdpi/exo_controls_pause.png diff --git a/library/src/main/res/drawable-xhdpi/exo_controls_play.png b/library/ui/src/main/res/drawable-xhdpi/exo_controls_play.png similarity index 100% rename from library/src/main/res/drawable-xhdpi/exo_controls_play.png rename to library/ui/src/main/res/drawable-xhdpi/exo_controls_play.png diff --git a/library/src/main/res/drawable-xhdpi/exo_controls_previous.png b/library/ui/src/main/res/drawable-xhdpi/exo_controls_previous.png similarity index 100% rename from library/src/main/res/drawable-xhdpi/exo_controls_previous.png rename to library/ui/src/main/res/drawable-xhdpi/exo_controls_previous.png diff --git a/library/src/main/res/drawable-xhdpi/exo_controls_rewind.png b/library/ui/src/main/res/drawable-xhdpi/exo_controls_rewind.png similarity index 100% rename from library/src/main/res/drawable-xhdpi/exo_controls_rewind.png rename to library/ui/src/main/res/drawable-xhdpi/exo_controls_rewind.png diff --git a/library/src/main/res/drawable-xxhdpi/exo_controls_fastforward.png b/library/ui/src/main/res/drawable-xxhdpi/exo_controls_fastforward.png similarity index 100% rename from library/src/main/res/drawable-xxhdpi/exo_controls_fastforward.png rename to library/ui/src/main/res/drawable-xxhdpi/exo_controls_fastforward.png diff --git a/library/src/main/res/drawable-xxhdpi/exo_controls_next.png b/library/ui/src/main/res/drawable-xxhdpi/exo_controls_next.png similarity index 100% rename from library/src/main/res/drawable-xxhdpi/exo_controls_next.png rename to library/ui/src/main/res/drawable-xxhdpi/exo_controls_next.png diff --git a/library/src/main/res/drawable-xxhdpi/exo_controls_pause.png b/library/ui/src/main/res/drawable-xxhdpi/exo_controls_pause.png similarity index 100% rename from library/src/main/res/drawable-xxhdpi/exo_controls_pause.png rename to library/ui/src/main/res/drawable-xxhdpi/exo_controls_pause.png diff --git a/library/src/main/res/drawable-xxhdpi/exo_controls_play.png b/library/ui/src/main/res/drawable-xxhdpi/exo_controls_play.png similarity index 100% rename from library/src/main/res/drawable-xxhdpi/exo_controls_play.png rename to library/ui/src/main/res/drawable-xxhdpi/exo_controls_play.png diff --git a/library/src/main/res/drawable-xxhdpi/exo_controls_previous.png b/library/ui/src/main/res/drawable-xxhdpi/exo_controls_previous.png similarity index 100% rename from library/src/main/res/drawable-xxhdpi/exo_controls_previous.png rename to library/ui/src/main/res/drawable-xxhdpi/exo_controls_previous.png diff --git a/library/src/main/res/drawable-xxhdpi/exo_controls_rewind.png b/library/ui/src/main/res/drawable-xxhdpi/exo_controls_rewind.png similarity index 100% rename from library/src/main/res/drawable-xxhdpi/exo_controls_rewind.png rename to library/ui/src/main/res/drawable-xxhdpi/exo_controls_rewind.png diff --git a/library/ui/src/main/res/drawable/exo_edit_mode_logo.xml b/library/ui/src/main/res/drawable/exo_edit_mode_logo.xml new file mode 100644 index 0000000000..80ff1844cd --- /dev/null +++ b/library/ui/src/main/res/drawable/exo_edit_mode_logo.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + diff --git a/library/src/main/res/layout/exo_playback_control_view.xml b/library/ui/src/main/res/layout/exo_playback_control_view.xml similarity index 94% rename from library/src/main/res/layout/exo_playback_control_view.xml rename to library/ui/src/main/res/layout/exo_playback_control_view.xml index f8ef5a6fdd..1d6267e7f0 100644 --- a/library/src/main/res/layout/exo_playback_control_view.xml +++ b/library/ui/src/main/res/layout/exo_playback_control_view.xml @@ -65,12 +65,11 @@ android:includeFontPadding="false" android:textColor="#FFBEBEBE"/> - + android:layout_height="26dp"/> + + - @@ -59,4 +60,16 @@ + + + + + + + + + + + + diff --git a/library/src/main/res/values/constants.xml b/library/ui/src/main/res/values/constants.xml similarity index 92% rename from library/src/main/res/values/constants.xml rename to library/ui/src/main/res/values/constants.xml index 5c86696ea0..eb94cacadd 100644 --- a/library/src/main/res/values/constants.xml +++ b/library/ui/src/main/res/values/constants.xml @@ -18,4 +18,6 @@ 71dp 52dp + #FFF4F3F0 + diff --git a/library/src/main/res/values/ids.xml b/library/ui/src/main/res/values/ids.xml similarity index 100% rename from library/src/main/res/values/ids.xml rename to library/ui/src/main/res/values/ids.xml diff --git a/library/src/main/res/values/strings.xml b/library/ui/src/main/res/values/strings.xml similarity index 100% rename from library/src/main/res/values/strings.xml rename to library/ui/src/main/res/values/strings.xml diff --git a/library/src/main/res/values/styles.xml b/library/ui/src/main/res/values/styles.xml similarity index 100% rename from library/src/main/res/values/styles.xml rename to library/ui/src/main/res/values/styles.xml diff --git a/playbacktests/build.gradle b/playbacktests/build.gradle index cb82d0a466..6a09eac49e 100644 --- a/playbacktests/build.gradle +++ b/playbacktests/build.gradle @@ -24,5 +24,7 @@ android { } dependencies { - compile project(':library') + compile project(':library-core') + androidTestCompile project(':library-dash') + androidTestCompile project(':library-hls') } diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java similarity index 99% rename from playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java rename to playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java index fc0701da8d..e7441362cf 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java @@ -28,9 +28,9 @@ import com.google.android.exoplayer2.util.Util; /** * Tests DASH playbacks using {@link ExoPlayer}. */ -public final class DashTest extends ActivityInstrumentationTestCase2 { +public final class DashStreamingTest extends ActivityInstrumentationTestCase2 { - private static final String TAG = "DashTest"; + private static final String TAG = "DashStreamingTest"; private static final ActionSchedule SEEKING_SCHEDULE = new ActionSchedule.Builder(TAG) .delay(10000).seek(15000) @@ -72,7 +72,7 @@ public final class DashTest extends ActivityInstrumentationTestCase2 drmSessionManager) { - SimpleExoPlayer player = new DebugSimpleExoPlayer(host, trackSelector, - new DefaultLoadControl(), drmSessionManager); + SimpleExoPlayer player = ExoPlayerFactory.newSimpleInstance( + new DebugRenderersFactory(host, drmSessionManager), trackSelector); player.setVideoSurface(surface); return player; } diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java index 99a6f3bef5..44f25b49d9 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java @@ -18,11 +18,15 @@ package com.google.android.exoplayer2.playbacktests.gts; import android.media.MediaDrm.MediaDrmStateException; import android.test.ActivityInstrumentationTestCase2; import android.util.Pair; +import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.OfflineLicenseHelper; import com.google.android.exoplayer2.playbacktests.util.ActionSchedule; import com.google.android.exoplayer2.playbacktests.util.HostActivity; +import com.google.android.exoplayer2.source.dash.DashUtil; +import com.google.android.exoplayer2.source.dash.manifest.DashManifest; +import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -72,7 +76,7 @@ public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCa releaseLicense(); } if (offlineLicenseHelper != null) { - offlineLicenseHelper.releaseResources(); + offlineLicenseHelper.release(); } offlineLicenseHelper = null; httpDataSourceFactory = null; @@ -89,7 +93,7 @@ public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCa testRunner.run(); // Renew license after playback should still work - offlineLicenseKeySetId = offlineLicenseHelper.renew(offlineLicenseKeySetId); + offlineLicenseKeySetId = offlineLicenseHelper.renewLicense(offlineLicenseKeySetId); Assert.assertNotNull(offlineLicenseKeySetId); } @@ -164,15 +168,18 @@ public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCa } private void downloadLicense() throws InterruptedException, DrmSessionException, IOException { - offlineLicenseKeySetId = offlineLicenseHelper.download( - httpDataSourceFactory.createDataSource(), DashTestData.WIDEVINE_H264_MANIFEST); + DataSource dataSource = httpDataSourceFactory.createDataSource(); + DashManifest dashManifest = DashUtil.loadManifest(dataSource, + DashTestData.WIDEVINE_H264_MANIFEST); + DrmInitData drmInitData = DashUtil.loadDrmInitData(dataSource, dashManifest.getPeriod(0)); + offlineLicenseKeySetId = offlineLicenseHelper.downloadLicense(drmInitData); Assert.assertNotNull(offlineLicenseKeySetId); Assert.assertTrue(offlineLicenseKeySetId.length > 0); testRunner.setOfflineLicenseKeySetId(offlineLicenseKeySetId); } private void releaseLicense() throws DrmSessionException { - offlineLicenseHelper.release(offlineLicenseKeySetId); + offlineLicenseHelper.releaseLicense(offlineLicenseKeySetId); offlineLicenseKeySetId = null; } diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugSimpleExoPlayer.java b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugRenderersFactory.java similarity index 80% rename from playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugSimpleExoPlayer.java rename to playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugRenderersFactory.java index c530ab63c1..6cb7673ebd 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugSimpleExoPlayer.java +++ b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugRenderersFactory.java @@ -18,39 +18,37 @@ package com.google.android.exoplayer2.playbacktests.util; import android.annotation.TargetApi; import android.content.Context; import android.os.Handler; +import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.LoadControl; import com.google.android.exoplayer2.Renderer; -import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; 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.trackselection.TrackSelector; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.util.ArrayList; /** - * A debug extension of {@link SimpleExoPlayer}. Provides video buffer timestamp assertions. + * A debug extension of {@link DefaultRenderersFactory}. Provides a video renderer that performs + * video buffer timestamp assertions. */ @TargetApi(16) -public class DebugSimpleExoPlayer extends SimpleExoPlayer { +public class DebugRenderersFactory extends DefaultRenderersFactory { - public DebugSimpleExoPlayer(Context context, TrackSelector trackSelector, - LoadControl loadControl, DrmSessionManager drmSessionManager) { - super(context, trackSelector, loadControl, drmSessionManager, - SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF, 0); + public DebugRenderersFactory(Context context, + DrmSessionManager drmSessionManager) { + super(context, drmSessionManager, DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF, 0); } @Override - protected void buildVideoRenderers(Context context, Handler mainHandler, - DrmSessionManager drmSessionManager, - @ExtensionRendererMode int extensionRendererMode, VideoRendererEventListener eventListener, - long allowedVideoJoiningTimeMs, ArrayList out) { + protected void buildVideoRenderers(Context context, + DrmSessionManager drmSessionManager, long allowedVideoJoiningTimeMs, + Handler eventHandler, VideoRendererEventListener eventListener, + @ExtensionRendererMode int extensionRendererMode, ArrayList out) { out.add(new DebugMediaCodecVideoRenderer(context, MediaCodecSelector.DEFAULT, - allowedVideoJoiningTimeMs, mainHandler, drmSessionManager, eventListener, + allowedVideoJoiningTimeMs, drmSessionManager, eventHandler, eventListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)); } @@ -70,9 +68,9 @@ public class DebugSimpleExoPlayer extends SimpleExoPlayer { private int minimumInsertIndex; public DebugMediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, - long allowedJoiningTimeMs, Handler eventHandler, - DrmSessionManager drmSessionManager, - VideoRendererEventListener eventListener, int maxDroppedFrameCountToNotify) { + long allowedJoiningTimeMs, DrmSessionManager drmSessionManager, + Handler eventHandler, VideoRendererEventListener eventListener, + int maxDroppedFrameCountToNotify) { super(context, mediaCodecSelector, allowedJoiningTimeMs, drmSessionManager, false, eventHandler, eventListener, maxDroppedFrameCountToNotify); } diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java index 87c55e9248..f48318687d 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java +++ b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java @@ -19,11 +19,13 @@ import android.os.Handler; import android.os.SystemClock; import android.util.Log; import android.view.Surface; -import com.google.android.exoplayer2.DefaultLoadControl; +import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.audio.AudioRendererEventListener; @@ -223,6 +225,11 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen // Do nothing. } + @Override + public final void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + // Do nothing. + } + @Override public final void onTimelineChanged(Timeline timeline, Object manifest) { // Do nothing. @@ -320,9 +327,9 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen protected SimpleExoPlayer buildExoPlayer(HostActivity host, Surface surface, MappingTrackSelector trackSelector, DrmSessionManager drmSessionManager) { - SimpleExoPlayer player = ExoPlayerFactory.newSimpleInstance(host, trackSelector, - new DefaultLoadControl(), drmSessionManager, SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF, - 0); + RenderersFactory renderersFactory = new DefaultRenderersFactory(host, drmSessionManager, + DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF, 0); + SimpleExoPlayer player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); player.setVideoSurface(surface); return player; } diff --git a/settings.gradle b/settings.gradle index b69c134fc4..544d2d4a21 100644 --- a/settings.gradle +++ b/settings.gradle @@ -12,6 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. include ':library' +include ':library-core' +include ':library-dash' +include ':library-hls' +include ':library-smoothstreaming' +include ':library-ui' include ':testutils' include ':demo' include ':playbacktests' @@ -24,6 +29,12 @@ include ':extension-vp9' // Uncomment the following line to use the Cronet Extension. // include ':extension-cronet' +project(':library').projectDir = new File(settingsDir, 'library/all') +project(':library-core').projectDir = new File(settingsDir, 'library/core') +project(':library-dash').projectDir = new File(settingsDir, 'library/dash') +project(':library-hls').projectDir = new File(settingsDir, 'library/hls') +project(':library-smoothstreaming').projectDir = new File(settingsDir, 'library/smoothstreaming') +project(':library-ui').projectDir = new File(settingsDir, 'library/ui') project(':extension-ffmpeg').projectDir = new File(settingsDir, 'extensions/ffmpeg') project(':extension-flac').projectDir = new File(settingsDir, 'extensions/flac') project(':extension-gvr').projectDir = new File(settingsDir, 'extensions/gvr') diff --git a/testutils/build.gradle b/testutils/build.gradle index a97c743384..5fea76f9c3 100644 --- a/testutils/build.gradle +++ b/testutils/build.gradle @@ -24,6 +24,6 @@ android { } dependencies { - compile project(':library') - compile 'org.mockito:mockito-core:1.9.5' + compile project(':library-core') + compile 'org.mockito:mockito-core:' + mockitoVersion } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java index 37947bb1ab..3e4d6b0440 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java @@ -21,45 +21,82 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; /** - * A fake {@link DataSource} capable of simulating various scenarios. - *

      - * The data that will be read from the source can be constructed by calling - * {@link Builder#appendReadData(byte[])}. Calls to {@link #read(byte[], int, int)} will not span - * the boundaries between arrays passed to successive calls, and hence the boundaries control the + * A fake {@link DataSource} capable of simulating various scenarios. It uses a {@link FakeDataSet} + * instance which determines the response to data access calls. + * + *

      Multiple fake data can be defined by {@link FakeDataSet#setData(String, byte[])} and {@link + * FakeDataSet#newData(String)} methods. It's also possible to define a default data by {@link + * FakeDataSet#newDefaultData()}. + * + *

      {@link FakeDataSet#newData(String)} and {@link FakeDataSet#newDefaultData()} return a {@link + * FakeData} instance which can be used to define specific results during {@link #read(byte[], int, + * int)} calls. + * + *

      The data that will be read from the source can be constructed by calling {@link + * FakeData#appendReadData(byte[])} Calls to {@link #read(byte[], int, int)} will not span the + * boundaries between arrays passed to successive calls, and hence the boundaries control the * positions at which read requests to the source may only be partially satisfied. - *

      - * Errors can be inserted by calling {@link Builder#appendReadError(IOException)}. An inserted error - * will be thrown from the first call to {@link #read(byte[], int, int)} that attempts to read from - * the corresponding position, and from all subsequent calls to {@link #read(byte[], int, int)} + * + *

      Errors can be inserted by calling {@link FakeData#appendReadError(IOException)}. An inserted + * error will be thrown from the first call to {@link #read(byte[], int, int)} that attempts to read + * from the corresponding position, and from all subsequent calls to {@link #read(byte[], int, int)} * until the source is closed. If the source is closed and re-opened having encountered an error, * that error will not be thrown again. + * + *

      Example usage: + * + *

      + *   // Create a FakeDataSource then add default data and two FakeData
      + *   // "test_file" throws an IOException when tried to be read until closed and reopened.
      + *   FakeDataSource fakeDataSource = new FakeDataSource();
      + *   fakeDataSource.getDataSet()
      + *       .newDefaultData()
      + *         .appendReadData(defaultData)
      + *         .endData()
      + *       .setData("http:///1", data1)
      + *       .newData("test_file")
      + *         .appendReadError(new IOException())
      + *         .appendReadData(data2);
      + *    // No need to call endData at the end
      + * 
      */ public final class FakeDataSource implements DataSource { - private final ArrayList segments; + private final FakeDataSet fakeDataSet; private final ArrayList openedDataSpecs; - private final boolean simulateUnknownLength; - private final long totalLength; - private Uri uri; private boolean opened; + private FakeData fakeData; private int currentSegmentIndex; private long bytesRemaining; - private FakeDataSource(boolean simulateUnknownLength, ArrayList segments) { - this.simulateUnknownLength = simulateUnknownLength; - this.segments = segments; - long totalLength = 0; - for (Segment segment : segments) { - totalLength += segment.length; - } - this.totalLength = totalLength; - openedDataSpecs = new ArrayList<>(); + public static Factory newFactory(final FakeDataSet fakeDataSet) { + return new Factory() { + @Override + public DataSource createDataSource() { + return new FakeDataSource(fakeDataSet); + } + }; + } + + public FakeDataSource() { + this(new FakeDataSet()); + } + + public FakeDataSource(FakeDataSet fakeDataSet) { + this.fakeDataSet = fakeDataSet; + this.openedDataSpecs = new ArrayList<>(); + } + + public FakeDataSet getDataSet() { + return fakeDataSet; } @Override @@ -69,6 +106,21 @@ public final class FakeDataSource implements DataSource { opened = true; uri = dataSpec.uri; openedDataSpecs.add(dataSpec); + + fakeData = fakeDataSet.getData(uri.toString()); + if (fakeData == null) { + throw new IOException("Data not found: " + dataSpec.uri); + } + + long totalLength = 0; + for (Segment segment : fakeData.segments) { + totalLength += segment.length; + } + + if (totalLength == 0) { + throw new IOException("Data is empty: " + dataSpec.uri); + } + // If the source knows that the request is unsatisfiable then fail. if (dataSpec.position >= totalLength || (dataSpec.length != C.LENGTH_UNSET && (dataSpec.position + dataSpec.length > totalLength))) { @@ -78,7 +130,7 @@ public final class FakeDataSource implements DataSource { boolean findingCurrentSegmentIndex = true; currentSegmentIndex = 0; int scannedLength = 0; - for (Segment segment : segments) { + for (Segment segment : fakeData.segments) { segment.bytesRead = (int) Math.min(Math.max(0, dataSpec.position - scannedLength), segment.length); scannedLength += segment.length; @@ -91,7 +143,7 @@ public final class FakeDataSource implements DataSource { // Configure bytesRemaining, and return. if (dataSpec.length == C.LENGTH_UNSET) { bytesRemaining = totalLength - dataSpec.position; - return simulateUnknownLength ? C.LENGTH_UNSET : bytesRemaining; + return fakeData.simulateUnknownLength ? C.LENGTH_UNSET : bytesRemaining; } else { bytesRemaining = dataSpec.length; return bytesRemaining; @@ -102,10 +154,10 @@ public final class FakeDataSource implements DataSource { public int read(byte[] buffer, int offset, int readLength) throws IOException { Assertions.checkState(opened); while (true) { - if (currentSegmentIndex == segments.size() || bytesRemaining == 0) { + if (currentSegmentIndex == fakeData.segments.size() || bytesRemaining == 0) { return C.RESULT_END_OF_INPUT; } - Segment current = segments.get(currentSegmentIndex); + Segment current = fakeData.segments.get(currentSegmentIndex); if (current.isErrorSegment()) { if (!current.exceptionCleared) { current.exceptionThrown = true; @@ -140,12 +192,13 @@ public final class FakeDataSource implements DataSource { Assertions.checkState(opened); opened = false; uri = null; - if (currentSegmentIndex < segments.size()) { - Segment current = segments.get(currentSegmentIndex); + if (currentSegmentIndex < fakeData.segments.size()) { + Segment current = fakeData.segments.get(currentSegmentIndex); if (current.isErrorSegment() && current.exceptionThrown) { current.exceptionCleared = true; } } + fakeData = null; } /** @@ -181,16 +234,24 @@ public final class FakeDataSource implements DataSource { } - /** - * Builder of {@link FakeDataSource} instances. - */ - public static final class Builder { + /** Container of fake data to be served by a {@link FakeDataSource}. */ + public static final class FakeData { + /** Uri of the data or null if this is the default FakeData. */ + public final String uri; private final ArrayList segments; + private final FakeDataSet dataSet; private boolean simulateUnknownLength; - public Builder() { - segments = new ArrayList<>(); + private FakeData(FakeDataSet dataSet, String uri) { + this.uri = uri; + this.segments = new ArrayList<>(); + this.dataSet = dataSet; + } + + /** Returns the {@link FakeDataSet} this FakeData belongs to. */ + public FakeDataSet endData() { + return dataSet; } /** @@ -199,7 +260,7 @@ public final class FakeDataSource implements DataSource { * the {@link DataSpec#length} of the argument, including the case where the length is equal to * {@link C#LENGTH_UNSET}. */ - public Builder setSimulateUnknownLength(boolean simulateUnknownLength) { + public FakeData setSimulateUnknownLength(boolean simulateUnknownLength) { this.simulateUnknownLength = simulateUnknownLength; return this; } @@ -207,7 +268,7 @@ public final class FakeDataSource implements DataSource { /** * Appends to the underlying data. */ - public Builder appendReadData(byte[] data) { + public FakeData appendReadData(byte[] data) { Assertions.checkState(data != null && data.length > 0); segments.add(new Segment(data, null)); return this; @@ -216,15 +277,64 @@ public final class FakeDataSource implements DataSource { /** * Appends an error in the underlying data. */ - public Builder appendReadError(IOException exception) { + public FakeData appendReadError(IOException exception) { segments.add(new Segment(null, exception)); return this; } - public FakeDataSource build() { - return new FakeDataSource(simulateUnknownLength, segments); + /** Returns the whole data added by {@link #appendReadData(byte[])}. */ + public byte[] getData() { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + for (Segment segment : segments) { + if (segment.data != null) { + try { + outputStream.write(segment.data); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + } + return outputStream.toByteArray(); + } + } + + /** A set of {@link FakeData} instances. */ + public static final class FakeDataSet { + + private final HashMap dataMap; + private FakeData defaultData; + + public FakeDataSet() { + dataMap = new HashMap<>(); } + public FakeData newDefaultData() { + defaultData = new FakeData(this, null); + return defaultData; + } + + public FakeData newData(String uri) { + FakeData data = new FakeData(this, uri); + dataMap.put(uri, data); + return data; + } + + public FakeDataSet setData(String uri, byte[] data) { + return newData(uri).appendReadData(data).endData(); + } + + public FakeData getData(String uri) { + FakeData data = dataMap.get(uri); + return data != null ? data : defaultData; + } + + public ArrayList getAllData() { + ArrayList fakeDatas = new ArrayList<>(dataMap.values()); + if (defaultData != null) { + fakeDatas.add(defaultData); + } + return fakeDatas; + } } }